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:
parent
20b1b9c5ba
commit
c93a49e7f2
5 changed files with 297 additions and 0 deletions
|
@ -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);
|
||||
|
|
3
libs/plugins-runtime/src/lib/utils/index.ts
Normal file
3
libs/plugins-runtime/src/lib/utils/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './object.util';
|
||||
export * from './parse-arr.util';
|
||||
export * from './parse.util';
|
42
libs/plugins-runtime/src/lib/utils/object.util.ts
Normal file
42
libs/plugins-runtime/src/lib/utils/object.util.ts
Normal 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());
|
||||
}
|
169
libs/plugins-runtime/src/lib/utils/parse-arr.util.ts
Normal file
169
libs/plugins-runtime/src/lib/utils/parse-arr.util.ts
Normal 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;
|
||||
}
|
80
libs/plugins-runtime/src/lib/utils/parse.util.ts
Normal file
80
libs/plugins-runtime/src/lib/utils/parse.util.ts
Normal 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));
|
||||
}
|
Loading…
Add table
Reference in a new issue