import { upperFirst } from 'lodash'; import { toWords } from 'number-to-words'; import { join, normalize, sep } from 'path'; import { Observable, Subject, UnaryFunction } from 'rxjs'; import { first, map, mapTo, mergeMap, shareReplay, tap } from 'rxjs/operators'; import { Options } from './../options'; import { camelCase, kebabCase } from './../utils/transliteration'; import { rxFindDir } from './rx.dir'; function _camelCase(aValue: string): string { return camelCase(aValue); } function _kebabCase(aValue: string): string { return kebabCase(aValue); } const TYPE_SUFFIX = 'Type'; const ELEMENT_SUFFIX = 'Element'; const ELEMENTS_PATH = 'elements'; const SINGLE_PREFIX = 'Single'; const MULTI_PREFIX = 'Multi'; const LEADING_DIGIT = /^(\d*)/g; function _createTypeComponentName(aName: string): string { // trim const trimmedName = aName.trim(); const fixed = trimmedName.replace(LEADING_DIGIT, (m, p) => (p && (p.length > 0)) ? toWords(p) + ' ' : ''); /** check if the layout component starts with a number, in this case * convert the number to a string */ const name = upperFirst(_camelCase(fixed)); return name.endsWith(TYPE_SUFFIX) ? name : name + TYPE_SUFFIX; } function _createElementComponentName(aName: string): string { // trim const trimmedName = aName.trim(); const fixed = trimmedName.replace(LEADING_DIGIT, (m, p) => (p && (p.length > 0)) ? toWords(p) + ' ' : ''); /** check if the layout component starts with a number, in this case * convert the number to a string */ const name = upperFirst(_camelCase(fixed)); return name.endsWith(TYPE_SUFFIX) ? name.substr(0, name.length - TYPE_SUFFIX.length) + ELEMENT_SUFFIX : name + ELEMENT_SUFFIX; } function _getPathForType(aType: any, aOptions: Options): string { // name of the layout folder segment let typeName = _createTypeComponentName(aType.name); if (typeName.endsWith(TYPE_SUFFIX)) { typeName = typeName.substr(0, typeName.length - TYPE_SUFFIX.length); } // layout folder const typeFolder = _kebabCase(typeName); // access the type properties const tags: string[] = aType.tags || []; // convert tags into segments const segments = aOptions.flat ? [] : tags.map(_kebabCase).sort(); segments.push(typeFolder); // return this path return segments.join(sep); } function _getTypePath(aType: any, aOptions: Options): string { // join return join(ELEMENTS_PATH, _getPathForType(aType, aOptions)); } export interface TypeClass { // directory for type folder: string; // selection coordinates type: any; // class names simpleTypeClass: string; typeElementClass: string; baseElementClass: string; singleElementClass: string; multiElementClass: string; // file names typeElementFile: string; // file references typeElementRef: string; } /** * Constructs the relevant information for a components class * * @param aDir base directory * @param aType type object * @param aOptions options * @param aLogger logger * * @return the component object */ function _initTypeClass(aDir: string, aType: any, aOptions: Options, aLogger: Subject): TypeClass { // check for the path suffix const typePath = _getTypePath(aType, aOptions); const type = aType; const typeName = type.name; // component names const typeClass = _createTypeComponentName(typeName); const simpleTypeClass = typeClass; const typeElementClass = typeClass.endsWith(TYPE_SUFFIX) ? typeClass.substr(0, typeClass.length - TYPE_SUFFIX.length) : typeClass; const baseElementClass = _createElementComponentName(typeName); const singleElementClass = SINGLE_PREFIX + baseElementClass; const multiElementClass = MULTI_PREFIX + baseElementClass; // references const typeElementRef = _camelCase(typeClass); // file names const typeElementFile = typeElementRef + '.ts'; // the base path for the layout const folder = normalize(join(aDir, typePath)); const result: TypeClass = { type, folder, simpleTypeClass, typeElementClass, baseElementClass, singleElementClass, multiElementClass, typeElementFile, typeElementRef }; // log this aLogger.next(`Element: ${typeName}, ElementClass: ${JSON.stringify(typeClass)}, Folder: ${folder}`); // ok return result; } /** * Finds the directory tha the component should be created in. Tests * each potential source directory for an existing component file. If non exists, uses the * first source directory * * @param aType type * @param aSources sources directory * * @return the selected source directory */ function _getTypesDir( aType: any, aSources: Observable, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { // check for the path suffix const typePath = _getTypePath(aType, aOptions); const className = _createTypeComponentName(aType.name); const relPath = join(typePath, _camelCase(className) + '.ts'); // map each source to a check return rxFindDir(aSources, relPath, aMkdirp).pipe( tap(dir => aLogger.next(`Selected type source directory [${dir}] for type [${className}].`)) ); } function _createTypeClass( aType: any, aSources: Observable, aOptions: Options, aMkdirp: UnaryFunction>, aLogger: Subject): Observable { // locate the component directory return _getTypesDir(aType, aSources, aOptions, aMkdirp, aLogger).pipe( first(), map(dir => _initTypeClass(dir, aType, aOptions, aLogger)), mergeMap(clz => aMkdirp(clz.folder).pipe(mapTo(clz))), shareReplay() ); } export class TypeRegistry { private typeRegistry: { [typeId: string]: Observable } = {}; private typeMap: { [typeId: string]: any } = {}; constructor( private aBaseFolders: Observable, private aOptions: Options, private aMkdirp: UnaryFunction>, private aLogger: Subject) { } registerType(aType: any): boolean { const type = aType; const id = type.id; if (this.typeMap[id]) { // do not create a type, twice return false; } // register this.typeMap[id] = type; // ok return true; } findTypeClass(aType: any): Observable { // the name const typeId = aType.id; // check if we are already searching let rxRunning = this.typeRegistry[typeId]; if (!rxRunning) { // try to find the folder rxRunning = this.typeRegistry[typeId] = _createTypeClass(aType, this.aBaseFolders, this.aOptions, this.aMkdirp, this.aLogger); } // ok return rxRunning; } }