asperjs-dev
Version:
A developmental branch of AsperJS, formerly Easy-Query.
454 lines (413 loc) • 21.4 kB
text/typescript
/// <reference path='./typings/index.d.ts' />
let fs = require('fs');
let _ = require('underscore');
class AsperJS {
/** *******************************************************************************************************************/
/** MEMBER DECLARATIONS ***********************************************************************************************/
/** *******************************************************************************************************************/
private default_language: string = 'scss';
private default_framework: string = 'bootstrap3';
private supported_languages: Array<string> = [ 'scss', 'sass', 'less' ];
private supported_measurements: Array<string> = [ 'px', 'pt', 'rem', 'em' ];
private supported_frameworks: Array<string> = [
'bootstrap2','bootstrap3','bootstrap4','foundation5','foundation6forapps',
'foundation6foremails','foundation6forsites','openframework','skeleton2'
];
private language: string = this.default_language;
private framework: string = this.default_framework;
private build_css: boolean = true;
private breakpoints: Array<number> = [];
private breakpoint_definitions: Object = {};
private rem_size: number = 16;
private generated_code: string;
private file_destination: string = './';
/** *******************************************************************************************************************/
/** INITIALIZATION ****************************************************************************************************/
/** *******************************************************************************************************************/
/**
* Documentation for constructor()
*
* Param can be an object or object array, a string, or null.
* If a string, it's value MUST be equivalent to a supported framework name.
* If an object, it may be a configuration JSON object or an Array.
* If an array, it may contain either numbers (which represent units of EM/REM)
* or strings representing non-EM/REM units (e.g., '768px').
* The value may also not be set at all, passing nothing to the constructor.
* In this case, it will by default fall back to the specified default for the
* current version.
*
* @param obj string|Object|Array<string>|Array<number>
*/
constructor(obj?: string|Array<any>|Object) {
if(!!obj) {
if (typeof obj === 'string' && this.validate_framework(obj)) {
this.load_framework(obj);
} else if (typeof obj === 'object' && obj.constructor === Array && this.validate_array((obj as Array<any>))) {
this.breakpoints = obj as Array<number>;
} else if (typeof obj === 'object') {
this.build_configuration_object(obj);
} else if (arguments.length === 0) {
this.load_framework(this.default_framework);
} else {
console.error('An error occured. Constructor parameter is invalid. Please see documentation for ' +
'instructions on valid constructor parameters.');
}
} else {
this.load_framework(this.default_framework);
}
this.build_stylesheet();
this.write_stylesheet();
}
/** *******************************************************************************************************************/
/** UTILITY FUNCTIONS *************************************************************************************************/
/** *******************************************************************************************************************/
/**
* @name split_measurement()
* @description Takes a pixel, point, em, or rem measurement (as a string) and returns an array containing the
* original string, the value as a string, and the measurement type (px, pt, em, rem) as a string, as indexes
* 0-2 respectively.
*
* @param {string} measurement
* @returns {{original: string, quantity: string, type: string}}
*/
private split_measurement = (measurement: string): Object => {
var parts = measurement.match(/(\d*\.?\d*)(.*)/);
return {
original: parts[0],
quantity: parts[1],
type: parts[2]
};
};
/**
* @name trim_rem()
* @description Takes a numerical value (rem unit) and subtracts 1px from it to serve as the ceiling for the next
* lower breakpoint.
* @param {number} rem
* @returns {number}
*/
private trim_rem = (rem: number): number => {
return rem - (1/this.rem_size);
};
/** *******************************************************************************************************************/
/** CONVERTER FUNCTIONS ***********************************************************************************************/
/** *******************************************************************************************************************/
/**
* @name convert_string_array()
* @description Converts an array of measurements (as strings) into an array of numbers representing
* measurements in REM values.
* @param array {Array<string>}
* @returns {Array<number>}
*/
private convert_string_array = (array: Array<string>): Array<number> => {
let newArray: Array<number> = [];
for(var x = 0; x < array.length; x++) {
let member = this.split_measurement(array[x]);
switch(member['type']) {
case 'px':
newArray.push(this.convert_px_to_rem(member['quantity']));
break;
case 'pt':
newArray.push(this.convert_pt_to_rem(member['quantity']));
break;
case 'em':
case 'rem':
default:
newArray.push(member['quantity']);
break;
}
}
return newArray;
};
/**
* @name convert_px_to_rem()
* @description Converts px values into rem values.
* @param px {number} Measurement in pixels.
* @returns {number}
*/
private convert_px_to_rem = (px: number): number => {
return px / this.rem_size;
};
/**
* @name convert_pt_to_rem()
* @description Converts pt values into rem values.
* @param pt {number} Measurement in (Adobe) pt values.
* @returns {number}
*/
private convert_pt_to_rem = (pt: number): number => {
return pt / 11.955168;
};
/** *******************************************************************************************************************/
/** VALIDATORS ********************************************************************************************************/
/** *******************************************************************************************************************/
/**
* @name validate_language()
* @description Validates that the selected language is a supported language.
* @param language {string}
* @returns {boolean}
*/
private validate_language = (language: string): boolean => {
return _.contains(this.supported_languages, language.toLowerCase());
};
/**
* @name validate_framework()
* @description Validates a passed framework versus those which are supported by the AsperJS utility.
* @param {string} framework
* @returns {boolean}
*/
private validate_framework = (framework: string): boolean => {
return _.contains(this.supported_frameworks, framework.toLowerCase());
};
/**
* @name validate_array()
* @description Validates that a passed array adheres to supported syntax (e.g., all numbers or properly formatted
* strings).
* @param {Array} array
* @returns {boolean}
*/
private validate_array = (array: Array<any>): boolean => {
var type = typeof array[0];
if(type === 'string') {
for(var x = 0; x < array.length; x++) {
if(typeof array[x]!==type || this.validate_string_array_measurements(this.split_measurement(array[x])[2]))
return false;
}
array = this.convert_string_array(array);
} else if(type === 'number') {
for(var x = 0; x < array.length; x++) {
if(typeof array[x] !== type || array[x] <= 0) return false;
}
} else {
return false;
}
return this.validate_number_array(array);
};
/**
* @name validate_number_array()
* @description Validates a passed array, specifically an array that is filled with only numbers.
* @param {Array<number>} array
* @returns {boolean}
*/
private validate_number_array = (array: Array<number>): boolean => {
for(var x = 0; x < array.length; x++) {
if(typeof array[x] !== 'number') {
return false;
}
}
return true;
};
/**
* @name validate_string_array_measurements()
* @description Validates a passed array, specifically an array that contains string representations of measurements.
* @param {string} type
* @returns {boolean}
*/
private validate_string_array_measurements = (type: string): boolean => {
return _.contains(this.supported_measurements, type.toLowerCase());
};
/**
* @name load_framework()
* @description Attempts to load a framework based on the passed framework name.
* @param {string} framework
* @returns {Array<number>}
*/
private load_framework = (framework: string): Array<number> => {
let loaded_framework: Array<number> = [];
try {
loaded_framework = require('./frameworks/' + framework);
} catch (e) {
console.error('An error occured: ' + e);
}
this.framework = framework;
this.breakpoints = loaded_framework;
return loaded_framework;
};
/** *******************************************************************************************************************/
/** BUILDERS **********************************************************************************************************/
/** *******************************************************************************************************************/
/**
* @name build_configuration_object()
* @description Takes a constructor parameter object, validates its contents, and either assigns given values or falls
* back to defaults. Automatically calls any necessary validation functions for proper instantiation.
* @param {Object} obj
*/
private build_configuration_object = (obj: Object) => {
if(obj.hasOwnProperty('framework') && obj.hasOwnProperty('breakpoints')) {
console.log('An error occured: If a custom constructor object is specified, you cannot specify both a custom ' +
'array of breakpoints and load a framework. Please select only one of the two.');
return;
}
this.language = obj.hasOwnProperty('lang') && this.validate_language(obj['lang'])
? obj['lang']
: this.default_language;
this.breakpoints = obj.hasOwnProperty('breakpoints') && this.validate_array(obj['framework'])
? obj['breakpoints']
: obj.hasOwnProperty('framework') && this.validate_framework(obj['framework'])
? this.load_framework(obj['framework'])
: this.load_framework(this.default_framework);
this.build_css = obj.hasOwnProperty('build_css') && typeof obj['build_css'] === "boolean" ?
obj['build_css']
: true;
this.file_destination = obj.hasOwnProperty('file_destination') && typeof obj['file_destination'] === 'string' ?
obj['file_destination']
: './';
this.rem_size = obj.hasOwnProperty('rem_size') && typeof obj['rem_size'] === 'number' && obj['rem_size'] > 0 ?
obj['rem_size']
: 16;
};
/**
* @name build_breakpoints()
* @description Takes the breakpoints given (either explicitly through the constructor or from a framework) and builds
* out the variable names for use in scss, less, and/or sass.
*/
private build_breakpoints = () => {
if(this.breakpoints) {
let count: number = Object.keys(this.breakpoints).length;
let breakpoint_labels: Array<string> = [];
if (count >= 1 && count <= 5) {
switch (count) {
case 1: breakpoint_labels = ['sm', 'lg']; break;
case 2: breakpoint_labels = ['sm', 'md', 'lg']; break;
case 3: breakpoint_labels = ['xs', 'sm', 'md', 'lg']; break;
case 4: breakpoint_labels = ['xs', 'sm', 'md', 'lg', 'xl']; break;
case 5: breakpoint_labels = ['xs', 'sm', 'md', 'lg', 'xl', 'xx']; break;
}
for(var x = 0; x < breakpoint_labels.length; x++) {
this.breakpoint_definitions[x] = {
id: breakpoint_labels[x],
min: x === 0 ? 0 : this.breakpoints[x-1],
max: x === breakpoint_labels.length - 1 ? 9999 : this.trim_rem(this.breakpoints[x])
};
}
} else {
console.error('An error occured. There must be at least one breakpoint but no more than 5.');
}
} else {
console.log('Breakpoints are undefined.');
}
};
/**
* @name build_stylesheet()
* @description This is the builder function that actually performs the building of the scss, less, or sass file for
* writing to the file system.
*/
private build_stylesheet = () => {
let pfx: string = this.language.toLowerCase() === 'less' ? '@' : '$';
let braceL: string = this.language.toLowerCase() === 'sass' ? '\n' : '{';
let braceR: string = this.language.toLowerCase() === 'sass' ? '\n' : '}';
let terminator: string = this.language === 'sass' ? '\n' : '\;\n';
let query: string = this.language.toLowerCase() === 'less' ? '@' : '#{$';
let interp_start: string = this.language.toLowerCase() === 'less' ? '@{' : '${';
let interp_end: string = this.language.toLowerCase() === 'less' ? '}' : '';
let end = this.language.toLowerCase() === 'less' ? '' : '}';
let caret = this.language.toLowerCase() === 'less' ? '~' : '';
let css_default: string = this.language.toLowerCase() === 'less' ? '' : ' !default';
this.generated_code = pfx + 'screen: ' + caret + '\'only screen\'' + css_default + terminator;
this.build_breakpoints();
for(var x = 0; x < Object.keys(this.breakpoint_definitions).length; x++) {
let curr_breakpoint = this.breakpoint_definitions[x];
let prev_breakpoint = this.build_css === true && x > 0
? this.breakpoint_definitions[x-1]
: null;
let next_breakpoint = this.build_css === true && x !== Object.keys(this.breakpoint_definitions).length - 1
? this.breakpoint_definitions[x+1]
: null;
this.generated_code += '\n' +
pfx + curr_breakpoint['id'] + '-only: ' + caret + '\"' +
interp_start + 'screen' + interp_end + ' and ' +
'(min-width: ' + curr_breakpoint['min'] + 'rem) and ' +
'(max-width: ' + curr_breakpoint['max'] + 'rem)\"' +
terminator;
if(next_breakpoint !== null && prev_breakpoint !== null) {
this.generated_code += build_stylesheet_inner_point(curr_breakpoint);
}
}
if(this.build_css) {
this.generated_code += '\n\n';
for(var x = 0; x < Object.keys(this.breakpoint_definitions).length; x++) {
let curr_breakpoint = this.breakpoint_definitions[x];
let prev_breakpoint = x > 0
? this.breakpoint_definitions[x-1]
: null;
let next_breakpoint = x !== Object.keys(this.breakpoint_definitions).length - 1
? this.breakpoint_definitions[x+1]
: null;
if(prev_breakpoint === null) {
let scope = next_breakpoint['max'] === 9999 ? '-only' : '-up';
this.generated_code +=
'.' + curr_breakpoint['id'] + '-only ' + braceL + '\n' +
'\t@media ' + query + next_breakpoint['id'] + scope + end + ' ' + braceL + '\n' +
'\t\tdisplay: none' + terminator +
'\t' + braceR +
'\n' + braceR + '\n\n';
}
if(next_breakpoint === null) {
let scope = prev_breakpoint['min'] === 0 ? '-only' : '-down';
this.generated_code +=
'.' + curr_breakpoint['id'] + '-only ' + braceL + '\n' +
'\t@media ' + query + prev_breakpoint['id'] + scope + end + ' ' + braceL + '\n' +
'\t\tdisplay: none' + terminator +
'\t' + braceR +
'\n' + braceR + '\n\n';
}
if(prev_breakpoint !== null && next_breakpoint !== null) {
let prev_scope = prev_breakpoint['min'] === 0 ? '-only' : '-down';
let next_scope = next_breakpoint['max'] === 9999 ? '-only' : '-up';
let scope = ['-only', '-up', '-down'];
for(var x = 0; x < scope.length; x++) {
this.generated_code +=
'.' + curr_breakpoint['id'] + scope[x] + ' ' + braceL + '\n' +
'\t@media ' + query + prev_breakpoint['id'] + prev_scope + end + ' ' + braceL + '\n' +
'\t\tdisplay: none' + terminator +
'\t' + braceR + '\n\n' +
'\t@media ' + query + next_breakpoint['id'] + next_scope + end + ' ' + braceL + '\n' +
'\t\tdisplay: none' + terminator +
'\t' + braceR +
'\n' + braceR + '\n\n';
}
}
}
}
/**
* @name build_stylesheet_inner_point()
* @description Private utility function exclusive to the AsperJS.build_stylesheet() function. This function
* generates any inner breakpoints (not the first or last breakpoint which only have <breakpoint>-only{}
* css associated with it. Any rules created here will have <breakpoint>-only, <breakpoint>-up, and
* <breakpoint>-down values.
* @param {Object} breakpoint
* @returns {string}
*/
function build_stylesheet_inner_point(breakpoint: Object): string {
return '\n' +
pfx + breakpoint['id'] + '-down: ' + caret + '\"' + interp_start + 'screen' + interp_end + ' and ' +
'(min-width: 0rem) and ' +
'(max-width: ' + breakpoint['max'] + 'rem)\"' + terminator +
'\n' +
pfx + breakpoint['id'] + '-up: ' + caret + '\"' + interp_start + 'screen' + interp_end + ' and ' +
'(min-width: ' + breakpoint['min'] + 'rem) and ' +
'(max-width: 9999rem)\"' + terminator;
}
};
/**
* @name write_stylesheet()
* @description This function takes the code compiled by the AsperJS.build_stylesheet() function and writes it to
* its respective file.
* @returns {string}
*/
public write_stylesheet = (): string => {
let dest:string = this.file_destination + '_asper.' + this.language;
try {
fs.writeFile(dest, this.generated_code, function(err) {
if (err) console.error('Error writing to file: ' + dest);
});
} catch(e) {
console.log('An Error Occured: ' + e.name + ': ' + e.message);
}
return dest;
};
}
/** *******************************************************************************************************************/
/** BUILD EXPORTABLE OBJECT FOR INSTANTIATION *************************************************************************/
/** *******************************************************************************************************************/
module.exports = (object) => {
return new AsperJS(object);
};