diff --git a/libs/plugins-runtime/src/index.ts b/libs/plugins-runtime/src/index.ts index 9698244..76db5d9 100644 --- a/libs/plugins-runtime/src/index.ts +++ b/libs/plugins-runtime/src/index.ts @@ -3,6 +3,7 @@ import './lib/plugin-modal'; import { ɵloadPlugin } from './lib/load-plugin'; import { setFileState, setPageState, setSelection } from './lib/api'; +import { parseFile } from './lib/utils'; repairIntrinsics({ evalTaming: 'unsafeEval', @@ -27,6 +28,8 @@ export function initialize(api: any) { // eslint-disable-next-line @typescript-eslint/no-explicit-any api.addListener('plugin-file', 'file', (file: any) => { + // console.log('File Changed (parsed):', parseFile(file)); + console.log('File Changed:', file); setFileState(file); diff --git a/libs/plugins-runtime/src/lib/utils/index.ts b/libs/plugins-runtime/src/lib/utils/index.ts new file mode 100644 index 0000000..baa9a5d --- /dev/null +++ b/libs/plugins-runtime/src/lib/utils/index.ts @@ -0,0 +1,3 @@ +export * from './object.util'; +export * from './parse-arr.util'; +export * from './parse.util'; diff --git a/libs/plugins-runtime/src/lib/utils/object.util.ts b/libs/plugins-runtime/src/lib/utils/object.util.ts new file mode 100644 index 0000000..3d05d28 --- /dev/null +++ b/libs/plugins-runtime/src/lib/utils/object.util.ts @@ -0,0 +1,42 @@ +/** + * Check if param is an object + */ +export function isObject(obj: unknown): boolean { + return typeof obj === 'object' && obj !== null && !Array.isArray(obj); +} + +/** + * Checks if an object have only one property, and if that + * property is the one passed as argument. + * + * examples checking property 'hello': + * + * { hello: 'world' } => true, + * + * { hello: 'world', foo: 'bar' } => false + */ +export function isSingleObjectWithProperty( + object: unknown, + property: string +): boolean { + if (isObject(object)) { + return ( + Object.keys(object as Record).length === 1 && + !!(object as Record)[property] + ); + } + + return false; +} + +/** + * Converts a string to camelCase from kebab-case and snake_case + */ +export function toCamelCase(str: string): string { + // Clean string from leading underscores and hyphens + const clean = str.replace(/^(_|-)_?/, ''); + + // Replace all underscores and hyphens followed by a character + // with that character in uppercase + return clean.replace(/(_|-)./g, (x) => x[1].toUpperCase()); +} diff --git a/libs/plugins-runtime/src/lib/utils/parse-arr.util.ts b/libs/plugins-runtime/src/lib/utils/parse-arr.util.ts new file mode 100644 index 0000000..f45fcc4 --- /dev/null +++ b/libs/plugins-runtime/src/lib/utils/parse-arr.util.ts @@ -0,0 +1,169 @@ +import { + isObject, + isSingleObjectWithProperty, + toCamelCase, +} from './object.util'; + +interface Name { + name: string; +} + +interface Uuid { + uuid: string; +} + +interface Arr { + arr: unknown[]; +} + +/** + * Checks if "arr" property can be turned into an object + */ +function toObject(arr: unknown[]): boolean { + return arr.some((a) => isSingleObjectWithProperty(a, 'name')); +} + +/** + * Checks if "arr" property can be turned into an array of objects + */ +function toArray(arr: unknown[]): boolean { + return ( + arr.every((a) => isObject(a)) && + arr.some((a) => isSingleObjectWithProperty(a, 'uuid')) && + arr.some((a) => isSingleObjectWithProperty(a, 'arr')) + ); +} + +/** + * Checks if "arr" needs cleaning and clean the leftovers uuid objects. + * + * It needs cleaning when uuid objects are redundant. + */ +function cleanUuid(arr: unknown[]): unknown[] { + const shouldClean = arr.some((a, index) => { + const next = arr[index + 1] as Record; + + return ( + isSingleObjectWithProperty(a, 'uuid') && + (next?.['id'] as Uuid)?.uuid === (a as Uuid).uuid + ); + }); + + if (shouldClean) { + return arr.filter((a) => !isSingleObjectWithProperty(a, 'uuid')); + } + + return arr; +} + +/** + * Parses a splitted "arr" back into an object if possible. + * + * If there are objects with a single property "name", that + * object and the next one are turned into a key-value pair. + * + * example: + * [{ name: 'foo' }, 'bar', { name: 'baz' }, 'qux'] => { foo: 'bar', baz: 'qux' } + */ +function arrToObject(arr: unknown[]): Record { + return arr.reduce( + (result: Record, value: unknown, index: number) => { + if (isSingleObjectWithProperty(value, 'name')) { + const next = arr[index + 1]; + if (!!next && !isSingleObjectWithProperty(next, 'name')) { + return { ...result, [toCamelCase((value as Name).name)]: next }; + } else { + return { ...result, [toCamelCase((value as Name).name)]: null }; + } + } + return { ...result }; + }, + {} + ); +} + +/** + * Recursively parses a splitted "arr" back into an array of + * objects with id and data properties. + * + * If there are objects with a single property "uuid", and + * the next one is an object with a single property "arr", + * it turns them into an object with id and data properties. + * + * It also checks for nested "arr" properties and turns them + * into an object with key-value pairs if possible. + * + * example: + * + * [{ uuid: 'foo' }, {arr: [{ name: 'bar' }, 'baz']}] => [{ id: 'foo', data: { bar: 'baz' } }] + */ +function arrToArray(arr: unknown[]): unknown[] { + return arr.reduce((result: unknown[], value: unknown, index: number) => { + if (isSingleObjectWithProperty(value, 'uuid')) { + const next = arr[index + 1]; + if (!!next && isSingleObjectWithProperty(next, 'arr')) { + const parsedArr = toObject((next as Arr).arr) + ? arrToObject((next as Arr).arr) + : toArray((next as Arr).arr) + ? arrToArray((next as Arr).arr) + : [...(next as Arr).arr]; + + return [...result, { id: (value as Uuid).uuid, data: parsedArr }]; + } + } + return [...result]; + }, []); +} + +/** + * Checks an "arr" property and decides which parse solution to use + */ +export function parseArrProperty( + arr: unknown[] +): unknown[] | Record { + if (toArray(arr)) { + return arrToArray(arr); + } else if (toObject(arr)) { + return arrToObject(arr); + } + + return cleanUuid(arr); +} + +/** + * Parses an object with an "arr" property + */ +export function parseObjArr(obj: unknown): unknown { + if (isSingleObjectWithProperty(obj, 'arr')) { + return parseArrProperty((obj as Arr)['arr'] as unknown[]); + } + + return obj; +} + +/** + * Checks if an array is a nested array of objects + */ +function isNestedArray(arr: unknown[]): boolean { + if ( + Array.isArray(arr) && + arr.every((a) => Array.isArray(a) && a.every((b) => isObject(b))) + ) { + return true; + } + + return false; +} + +/** + * If an array is nested, it flattens it. + * + * example + * [[1, 2], [3, 4]] => [1, 2, 3, 4] + */ +export function flattenNestedArrays(arr: unknown[]): unknown { + if (isNestedArray(arr)) { + return arr.flatMap((innerArray) => innerArray); + } + return arr; +} diff --git a/libs/plugins-runtime/src/lib/utils/parse.util.ts b/libs/plugins-runtime/src/lib/utils/parse.util.ts new file mode 100644 index 0000000..4c8a0dc --- /dev/null +++ b/libs/plugins-runtime/src/lib/utils/parse.util.ts @@ -0,0 +1,80 @@ +import { + isObject, + isSingleObjectWithProperty, + toCamelCase, +} from './object.util'; +import { flattenNestedArrays, parseObjArr } from './parse-arr.util'; + +/** + * Recursively cleans an object from unnecesary properties + * and converts snake_case and kebab-case to camelCase + */ +export function cleanObject( + obj: unknown +): Record | Record[] { + if (Array.isArray(obj)) { + return obj + .filter((p) => p !== null) + .map((v: Record) => cleanObject(v)) as Record< + string, + unknown + >[]; + } else if (isObject(obj)) { + return Object.keys(obj as Record) + .filter( + (key) => + !/^(\$|cljs\$|__hash|_hash|bitmap|meta|ns|fqn|cnt|shift|edit|has_nil_QMARK_|nil_val)/g.test( + key + ) + ) + .reduce( + (result, key) => ({ + ...result, + [toCamelCase(key)]: cleanObject( + (obj as Record)[key] + ), + }), + {} + ); + } + return obj as Record; +} + +/** + * Recursively checks for "arr" properties and parses them + */ +export function parseObject(obj: unknown): unknown { + // If it's an array, parse each element + if (Array.isArray(obj)) { + const parsedArray = obj.map((v: Record) => parseObject(v)); + + // Flatten nested arrays if necessary + return flattenNestedArrays(parsedArray); + } + + // If it's an object with only property 'arr', parse it + if (isSingleObjectWithProperty(obj, 'arr')) { + const parsed = parseObjArr(obj as Record); + return parseObject(parsed); + } + + // If it's an object, parse each property + if (isObject(obj)) { + return Object.keys(obj as Record).reduce( + (result, key) => ({ + ...result, + [key]: parseObject((obj as Record)[key]), + }), + {} + ); + } + + return obj; +} + +/** + * Parse a file object into a more typescript friendly object + */ +export function parseFile(file: unknown): unknown { + return parseObject(cleanObject(file)); +}