UNPKG

asperjs-dev

Version:

A developmental branch of AsperJS, formerly Easy-Query.

454 lines (413 loc) 21.4 kB
/// <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); };