0
Fork 0
mirror of https://github.com/penpot/penpot-plugins.git synced 2025-02-12 18:18:55 -05:00

feat: t#6806 parse file

This commit is contained in:
María Valderrama 2024-02-29 08:34:13 +01:00
parent 20b1b9c5ba
commit c93a49e7f2
5 changed files with 297 additions and 0 deletions

View file

@ -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);

View file

@ -0,0 +1,3 @@
export * from './object.util';
export * from './parse-arr.util';
export * from './parse.util';

View file

@ -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<string, unknown>).length === 1 &&
!!(object as Record<string, unknown>)[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());
}

View file

@ -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<string, unknown>;
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<string, unknown> {
return arr.reduce(
(result: Record<string, unknown>, 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<string, unknown> {
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;
}

View file

@ -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<string, unknown> | Record<string, unknown>[] {
if (Array.isArray(obj)) {
return obj
.filter((p) => p !== null)
.map((v: Record<string, unknown>) => cleanObject(v)) as Record<
string,
unknown
>[];
} else if (isObject(obj)) {
return Object.keys(obj as Record<string, unknown>)
.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<string, unknown>)[key]
),
}),
{}
);
}
return obj as Record<string, unknown>;
}
/**
* 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<string, unknown>) => 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<string, unknown>);
return parseObject(parsed);
}
// If it's an object, parse each property
if (isObject(obj)) {
return Object.keys(obj as Record<string, unknown>).reduce(
(result, key) => ({
...result,
[key]: parseObject((obj as Record<string, unknown>)[key]),
}),
{}
);
}
return obj;
}
/**
* Parse a file object into a more typescript friendly object
*/
export function parseFile(file: unknown): unknown {
return parseObject(cleanObject(file));
}