0
Fork 0
mirror of https://github.com/penpot/penpot-plugins.git synced 2025-01-06 14:50:21 -05:00

fix(parser): clean up

This commit is contained in:
María Valderrama 2024-03-05 14:50:28 +01:00
parent eb715ddb7c
commit efca5d4d1a
23 changed files with 676 additions and 466 deletions

View file

@ -1,59 +1,105 @@
# Parser
This library exports `parse()` and `getSelectedUuids()` funtions and some models like `ParsedFile`, `ParsedPage` or `UnparsedSelection`.
This library exports `cleanObject()` and `getSelectedUuids()` funtions and the model `Selection`.
The `parse()` function cleans up and transforms a penpot object into a more typescript friendly object. It returns a `ParsedData` object that can be casted as `ParsedFile` or `ParsedPage`. Note that `ParsedData` is the parent interface and includes all `ParsedFile` and `ParsedPage` properties.
The `cleanObject()` function cleans up objects from useless properties and transforms the remaining ones to camelCase. It returns `unknown`.
Most of the properties are optional and may or may not be present in your result, you should access them with care.
The `getSelectedUuids()` functions, given an `Selection` object, returns the selected Uuids as an array of string.
The `getSelectedUuids()` functions, given an `UnparsedSelection` object, returns the selected Uuids as an array of string.
## Helpers
## Use
### File Helper
Import the parse function and the desired models from plugins data parser.
#### File Helper functions
Examples:
- `setData()`
- `parse()`
You can either pass the data in the constructor or use the `setData()` function.
example:
```ts
import { parse, ParsedFile } from 'plugins-parser';
[...]
const parsedFile: ParsedFile = parse(file);
const color = parsedFile.data.colors?.[0];
/** color:
* {
* "path": "Background",
* "color": "#1c1b1f",
* "fileId": "f13ed095-e13f-808c-8002-2830d45911f7",
* "name": "On Background",
* "opacity": 1,
* "id": "136eece0-40ab-8002-8002-296771ace070"
* }
*/
const fileHelper = new FileHelper();
fileHelper.setData(data);
```
- `getSelectedUuids()`
or
```ts
import { getSelectedUuids, UnparsedSelection } from 'plugins-parser';
[...]
const selection: UnparsedSelection = { [...] };
const ids: string[] = getSelectedUuids(selection); =>
/** ids:
* [
* "4fa12080-0d58-80a3-8002-3a2344356e7c",
* "d9a61226-8431-8080-8002-5aea35acc724"
* ]
*/
const fileHelper = new FileHelper(data);
```
- `getCleanData()`
Gets the cleaned up data. It deletes useless properties and and transforms the remaining ones to camelCase.
example:
```ts
const clean = fileHelper.getCleanData();
```
### Page Helper
#### Page Helper functions
- `setData()`
You can either pass the data in the constructor or use the `setData()` function.
example:
```ts
const pageHelper = new PageHelper();
pageHelper.setData(data);
```
or
```ts
const pageHelper = new PageHelper(data);
```
- `getCleanData()`
Gets the cleaned up data. It deletes useless properties and and transforms the remaining ones to camelCase.
example:
```ts
const clean = pageHelper.getCleanData();
```
- `getObjectsArray()`
Returns the objects array, which can contain heavily nested arrays with objects data.
example:
```ts
const objects = pageHelper.getObjectsArray();
```
- `getObjectById(id: string)`
Returns an object by given uuid. The object is cleaned up and formatted as a `PObject`.
```ts
const obj: PObject = pageHelper.getObjectById(
'3aba0744-11fe-4c41-80fb-1b42aa7ef3e5'
);
```
### Selection Helper
#### Selection Helper functions
- `static getUuids(selection: Selection)`
Returns the selected items in an array.
example:
```ts
const ids = SelectionHelper.getUuids(selection);
```

View file

@ -1,3 +1,3 @@
export { parse, getSelectedUuids } from './lib/utils';
export { getSelectedUuids, cleanObject, getPartialState } from './lib/utils';
export * from './lib/models';
export { getPartialState } from './lib/utils/parse-state';
export * from './lib/helpers';

View file

@ -0,0 +1,22 @@
import { cleanObject } from '../utils';
/**
* WIP
*/
export class FileHelper {
private data: unknown = null;
public constructor(data?: unknown) {
if (data) {
this.setData(data);
}
}
public setData(data: unknown): void {
this.data = cleanObject(data);
}
public getCleanData(): unknown {
return this.data;
}
}

View file

@ -0,0 +1,3 @@
export * from './file.helper';
export * from './page.helper';
export * from './selection.helper';

View file

@ -0,0 +1,97 @@
import { cleanObject, parseObject, isObject } from '../utils';
import { PObject } from '../models';
import { Name, Uuid } from '../utils/models/util.model';
interface PageArr {
data?: {
arr?: unknown[];
};
[key: string]: unknown;
}
interface PageObject {
root?: {
arr?: unknown[];
};
[key: string]: unknown;
}
/**
* WIP
*/
export class PageHelper {
private data: unknown = null;
private objects: unknown[] = [];
public constructor(data?: unknown) {
if (data) {
this.setData(data);
}
}
public setData(data: unknown): void {
this.data = cleanObject(data);
this.objects = this.getPageObjects(this.data as PageArr);
}
public getCleanData(): unknown {
return this.data;
}
public getObjectsArray(): unknown[] {
return this.objects;
}
public getObjectById(id: string): PObject | null {
if (!this.objects) {
return null;
}
const foundObject = this.findObject(this.objects, id);
return parseObject(foundObject) as PObject;
}
private getPageObjects(obj: PageArr): unknown[] {
const dataArr = obj?.data?.arr;
const objectNameIndex = dataArr?.findIndex(
(item) => isObject(item) && (item as Name)?.name === 'objects'
);
if (!objectNameIndex) {
return [];
}
const objects = dataArr?.[objectNameIndex + 1] as PageObject;
return (isObject(objects) && objects?.root?.arr) || [];
}
private findObject(
data: unknown,
id: string
): Record<string, unknown> | null {
if (Array.isArray(data)) {
for (const item of data) {
const foundObject = this.findObject(item, id);
if (foundObject !== null) {
return foundObject;
}
}
} else if (isObject(data)) {
const obj = data as Record<string, unknown>;
if ((obj?.['id'] as Uuid)?.uuid === id || obj?.['id'] === id) {
return obj;
}
for (const key of Object.keys(obj)) {
const foundObject = this.findObject(obj[key], id);
if (foundObject !== null) {
return foundObject;
}
}
}
return null;
}
}

View file

@ -0,0 +1,15 @@
import { Selection } from '../models';
/**
* WIP
*/
export class SelectionHelper {
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
static getUuids(selection: Selection): string[] {
const root = selection?.linked_map?.delegate_map?.root?.arr;
return (root?.filter((r) => r?.uuid).map((r) => r.uuid) as string[]) || [];
}
}

View file

@ -0,0 +1,181 @@
import { Point } from '.';
export interface Extmap {
fillColor?: string;
strokeStyle?: null | string;
none?: unknown;
strokeColor?: string;
content?: ExtmapContent[];
strokeOpacity?: number;
strokeAlignment?: unknown;
center?: null | ExtmapInner;
proportionLock?: null | boolean;
constraintsV?: unknown;
constraintsH?: unknown;
leftright?: unknown;
strokes?: Stroke[];
proportion?: number;
fills?: Fill[];
fillOpacity?: number;
growType?: unknown;
touched?: Touched;
shapeRef?: string;
topbottom?: unknown;
positionData?: PositionData[];
overflowText?: unknown;
fixed?: unknown;
hidden?: null | boolean;
componentId?: string;
flipX?: unknown;
componentFile?: string;
flipY?: unknown;
shapes?: string[];
inner?: ExtmapInner;
strokeWidth?: number;
shadow: Shadow[];
componentRoot?: boolean;
rx?: number;
ry?: number;
autoWidth?: unknown;
top?: Top;
hideInViewer: null | boolean;
exports?: null | Export[];
thumbnail?: string;
}
export interface Export {
type: null | string;
jpeg: unknown;
suffix: null | string;
scale: number;
}
export interface Top {
constraintsH: unknown;
leftRight: unknown;
hidden: null | boolean;
}
export interface Shadow {
color: Partial<Color>;
spread: unknown;
offsetY: number;
style: unknown;
dropShadow: unknown;
blur: number;
hidden: null | boolean;
opacity: null | number;
id: string;
offsetX: null | number;
}
export interface ExtmapInner {
shapeRef: string;
proportionLock: boolean;
}
export interface Touched {
hashMap: HashMap;
}
export interface HashMap {
hashMap: GeometryGroup;
}
export interface GeometryGroup {
geometryGroup: unknown;
}
export interface PositionData {
y?: number;
fontStyle?: string;
textTransform?: string;
fontSize?: string;
fontWeight?: string;
width?: number;
textDecoration?: string;
letterSpacing?: string;
x?: number;
fills?: Fill[];
direction?: string;
fontFamily?: string;
height?: number;
text?: string;
extmap?: ExtmapContent;
x1?: number;
y1?: number;
x2?: number;
y2?: number;
}
export interface ExtmapBottom {
blur: Blur;
strokeWidth: number;
}
export interface Fill {
fillColor: string;
fillOpacity: number;
fillColorRefFile?: string;
fillColorRefId?: string;
}
export interface Stroke {
strokeStyle: unknown;
none: unknown;
strokeColor: string;
strokeOpacity: number;
strokeAlignment: unknown;
center: unknown;
strokeWidth: number;
}
export interface Blur {
id: string;
type: unknown;
layerBlur: unknown;
value: number;
hidden: unknown;
}
export interface ExtmapContent {
command?: unknown;
moveTo?: unknown;
relative?: unknown;
params?: Point;
key?: string;
type?: string;
children?: ExtmapContent[];
fillColor?: string;
fillOpacity?: number;
fontFamily?: string;
fontId?: string;
fontSize?: string;
fontStyle?: string;
fontVariantId?: string;
fontWeight?: string;
text?: string;
textDecoration?: string;
textTransform?: string;
verticalAlign?: string;
direction?: string;
fills?: Fill[];
letterSpacing?: string;
}
export interface ExtmapContentPoint {
x: number;
y: number;
c1x?: number;
c1y?: number;
c2x?: number;
c2y?: number;
}
export interface Color {
color: string;
opacity: number;
id: string;
name: string;
fileId: string;
path: string | null;
}

View file

@ -1,13 +0,0 @@
import { Data, ParsedData } from '.';
export type FileDataType =
| 'colors'
| 'typographies'
| 'pages'
| 'media'
| 'pagesIndex'
| 'components';
export interface ParsedFile extends Omit<ParsedData, 'data'> {
data: Pick<Data, FileDataType>;
}

View file

@ -1,5 +1,3 @@
export * from './parsed.model';
export * from './utils.model';
export * from './file.model';
export * from './page.model';
export * from './selection.model';
export * from './object.model';
export * from './extmap.model';

View file

@ -0,0 +1,44 @@
import { Extmap } from '.';
export interface PObject {
id: string;
name: string;
type: string;
x: number;
y: number;
width: number;
height: number;
rotation: number;
selrect: Selrect;
points: Point[];
transform: Transform;
transformInverse: Transform;
parentId: string;
frameId: string;
extmap: Extmap;
}
export interface Selrect {
x: number;
y: number;
width: number;
height: number;
x1: number;
y1: number;
x2: number;
y2: number;
}
export interface Point {
x: number;
y: number;
}
export interface Transform {
a: number;
b: number;
c: number;
d: number;
e: number;
f: number;
}

View file

@ -1,7 +0,0 @@
import { Data, ParsedData } from '.';
export type PageDataType = 'options' | 'objects' | 'name' | 'id';
export interface ParsedPage extends Omit<ParsedData, 'data'> {
data: Pick<Data, PageDataType>;
}

View file

@ -1,125 +0,0 @@
export interface ParsedData {
id: string;
name: string;
data: Data;
}
export interface Data {
id: string;
version: number;
colors?: Color[];
typographies?: Typhography[];
pages?: RootTail<unknown, string[]>; // Tail is an array of uuid (string)
pagesIndex?: PageIndex[];
components?: Components[];
media?: Media[];
options?: Option[];
objects?: ObjectI[];
name?: string;
}
export interface Color {
color: string;
opacity: number;
id: string;
name: string;
fileId: string;
path: string | null;
}
export interface Typhography {
lineHeight: string;
path: string | null;
fontStyle: string;
textTransform: string;
fontId: string;
fontSize: string;
fontWeight: string;
name: string;
fontVariantId: string;
id: string;
letterSpacing: string;
fontFamily: string;
modifiedAt?: Date;
}
export interface PageIndex {
options: Option[];
name: string;
objects: ObjectI[];
}
export interface Option {
position: number;
frameId: string;
id: string;
axis: unknown;
x: unknown;
}
export interface Components {
id: string;
name: string;
path: string;
objects: ObjectI[];
}
export interface ObjectI {
id: string;
name: string;
type: string;
x: number;
y: number;
width: number;
height: number;
rotation: 0;
selrect: Selrect;
points: RootTail<unknown, Point[]>;
transform: Transform;
transformInverse: Transform;
parentId: null | string;
frameId: null | string;
}
export interface Selrect {
x: number;
y: number;
width: number;
height: number;
x1: number;
y1: number;
x2: number;
y2: number;
}
export interface Point {
x: number;
y: number;
}
export interface Transform {
a: number;
b: number;
c: number;
d: number;
e: number;
f: number;
}
export interface Media {
id: string;
name: string;
width: number;
height: number;
mtype: string;
path: string;
}
/*****************
* Generic types *
*****************/
export interface RootTail<R, T> {
root: R;
tail: T;
}

View file

@ -1,4 +1,4 @@
export interface UnparsedSelection extends CljValues {
export interface Selection extends CljValues {
linked_map: SelLinkedMap;
}

View file

@ -0,0 +1,88 @@
import { isObject, toCamelCase, isSingleObjectWithProperty } from '.';
import { Arr, Name } from './models/util.model';
/**
* Checks if "arr" property can be turned into an object
*/
function toObject(arr: unknown): boolean {
return (
Array.isArray(arr) && arr.some((a) => isSingleObjectWithProperty(a, 'name'))
);
}
/**
* Checks if "arr" property can be turned into an array of objects
*/
function toArray(arr: unknown): boolean {
return (
Array.isArray(arr) &&
arr.every((a) => isObject(a)) &&
arr.every(
(a) =>
isSingleObjectWithProperty(a, 'uuid') ||
isSingleObjectWithProperty(a, '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> | null {
return (
(Array.isArray(arr) &&
arr.reduce(
(result: Record<string, unknown>, value: unknown, index: number) => {
if (isSingleObjectWithProperty(value, 'name')) {
const next = arr[index + 1] as unknown;
if (!!next && !isSingleObjectWithProperty(next, 'name')) {
return { ...result, [toCamelCase((value as Name)?.name)]: next };
} else {
return { ...result, [toCamelCase((value as Name)?.name)]: null };
}
}
return { ...result };
},
{}
)) ||
null
);
}
/**
* Parses a splitted "arr" back into an array of objects.
*
*/
function arrToArray(arr: unknown): unknown[] | null {
return (
(Array.isArray(arr) &&
arr.reduce((result: unknown[], value: unknown) => {
if (isSingleObjectWithProperty(value, 'arr')) {
return [...result, { ...(value as Arr)?.arr }];
}
return [...result];
}, [])) ||
null
);
}
/**
* Checks an "arr" property and decides which parse solution to use
*/
export function parseArrProperty(
arr: unknown[]
): unknown[] | Record<string, unknown> | null {
if (toArray(arr)) {
return arrToArray(arr);
} else if (toObject(arr)) {
return arrToObject(arr);
}
return arr;
}

View file

@ -1,5 +1,6 @@
export * from './object.util';
export * from './parse-arr.util';
export * from './parse.util';
export * from './parse-properties.util';
export * from './selected.util';
export * from './arr.util';
export * from './string.util';
export * from './parse-state';

View file

@ -6,13 +6,48 @@ export function isObject(obj: unknown): boolean {
}
/**
* Converts a string to camelCase from kebab-case and snake_case
* Check if param is an object with a single property
*/
export function toCamelCase(str: string): string {
// Clean string from leading underscores and hyphens
const clean = str.replace(/^(_|-)_?/, '');
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]
);
}
// Replace all underscores and hyphens followed by a character
// with that character in uppercase
return clean.replace(/(_|-)./g, (x) => x[1].toUpperCase());
return false;
}
/**
* Check if param is an object with a single property from a list
*/
export function isSingleObjectWithProperties(
object: unknown,
properties: string[]
): boolean {
if (isObject(object)) {
const keys = Object.keys(object as Record<string, unknown>);
if (keys.length === 1) {
return properties.includes(keys[0]);
}
}
return false;
}
/**
* Check if param is a root-tail object
*/
export function isRootTail(obj: unknown): boolean {
if (isObject(obj)) {
const keys = Object.keys(obj as Record<string, unknown>);
return keys.length === 2 && keys.includes('root') && keys.includes('tail');
}
return false;
}

View file

@ -1,160 +0,0 @@
import { Arr, Name, Uuid } from '../models';
import { isObject, toCamelCase } from './object.util';
import { isSingleObjectWithProperty } from './parse-properties.util';
/**
* 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, { ...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']);
}
return obj;
}
/**
* Checks if an array is a nested array of objects
*
* It also checks and filter empty nested arrays
*/
function isNestedArray(arr: unknown[]): boolean {
if (Array.isArray(arr) && arr.every((a) => Array.isArray(a))) {
// Filter empty nested arrays
const filtered = arr.filter((a) => (a as unknown[]).length > 0);
// Check if every nested array is an array of objects
return filtered.every(
(a) => Array.isArray(a) && a.every((b) => isObject(b))
);
}
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

@ -1,55 +0,0 @@
import { isObject } from '.';
/**
* 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;
}
export function isSingleObjectWithProperties(
object: unknown,
properties: string[]
): boolean {
if (isObject(object)) {
const keys = Object.keys(object as Record<string, unknown>);
if (keys.length === 1) {
return properties.includes(keys[0]);
}
}
return false;
}
export function parseSingleProperties(
obj: unknown,
properties: string[]
): unknown {
let result = obj;
properties.forEach((property) => {
if (isSingleObjectWithProperty(obj, property)) {
result = (obj as Record<string, unknown>)[property];
}
});
return result;
}

View file

@ -1,6 +1,6 @@
/* eslint-disable */
import { toCamelCase } from './object.util';
import { toCamelCase } from '.';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type UknowCljs = any;

View file

@ -1,11 +1,52 @@
import { ParsedData } from '../models/parsed.model';
import { isObject, toCamelCase } from './object.util';
import { flattenNestedArrays, parseObjArr } from './parse-arr.util';
import {
isObject,
toCamelCase,
parseArrProperty,
isRootTail,
isSingleObjectWithProperties,
isSingleObjectWithProperty,
parseSingleProperties,
} from './parse-properties.util';
} from '.';
import { Arr } from './models/util.model';
export function parseSingleProperties(
obj: unknown,
properties: string[]
): unknown {
let result = obj;
properties.forEach((property) => {
if (isSingleObjectWithProperty(obj, property)) {
result = (obj as Record<string, unknown>)[property];
}
});
return result;
}
export function parseRootTail(obj: unknown): unknown {
if (isObject(obj) && isRootTail(obj)) {
const { root, tail } = obj as Record<string, unknown>;
const hasRoot = Array.isArray(root) && root?.length > 0;
const hasTail = Array.isArray(tail) && tail?.length > 0;
if (hasRoot && hasTail) {
return obj;
}
if (hasTail) {
return tail;
}
if (hasRoot) {
return root;
}
return [];
}
return obj;
}
/**
* Recursively cleans an object from unnecesary properties
@ -23,45 +64,29 @@ export function cleanObject(obj: unknown): unknown {
return Object.keys(obj as Record<string, unknown>)
.filter(
(key) =>
!/^(\$|cljs\$|__hash|_hash|bitmap|meta|extmap|ns|fqn|cnt|shift|edit|has_nil_QMARK_|nil_val)/g.test(
!/^(cljs\$|\$hash|ts|ry|\$meta|__hash|_hash|bitmap|meta|ns|fqn|cnt|shift|edit|has_nil_QMARK_|nil_val)/g.test(
key
)
)
.reduce(
(result, key) => ({
.reduce((result, key) => {
const value = (obj as Record<string, unknown>)[key];
if (['extmap', '$extmap'].includes(key) && value === null) {
return { ...result };
}
return {
...result,
[toCamelCase(key)]: cleanObject(
(obj as Record<string, unknown>)[key]
),
}),
{}
);
};
}, {});
}
return obj as Record<string, unknown>;
}
/**
* Recursively checks for "arr" properties and parses them
*
* It also checks for useless one-property objects like uuid or root
*/
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 with only properties singleProperties, parse them
const singleProperties = ['root', 'uuid', 'name', 'guides'];
const singleProperties = ['root', 'name', 'uuid', 'guides'];
if (isSingleObjectWithProperties(obj, singleProperties)) {
const parsed = parseSingleProperties(
obj as Record<string, unknown>,
@ -70,23 +95,30 @@ export function parseObject(obj: unknown): unknown {
return parseObject(parsed);
}
if (isSingleObjectWithProperty(obj, 'arr')) {
const parsed = parseArrProperty((obj as Arr).arr);
return parseObject(parsed);
}
if (isRootTail(obj)) {
const parsed = parseRootTail(obj);
return parseObject(parsed);
}
// If it's an array, parse each element
if (Array.isArray(obj)) {
return obj.map((v: Record<string, unknown>) => parseObject(v));
}
// If it's an object, parse each property
if (isObject(obj)) {
return Object.keys(obj as Record<string, unknown>).reduce(
(result, key) => ({
return Object.keys(obj as Record<string, unknown>).reduce((result, key) => {
return {
...result,
[key]: parseObject((obj as Record<string, unknown>)[key]),
}),
{}
);
};
}, {});
}
return obj;
}
/**
* Parse object into a more typescript friendly object
*/
export function parse(file: unknown): ParsedData {
return parseObject(cleanObject(file)) as ParsedData;
}

View file

@ -1,9 +1,9 @@
import { UnparsedSelection } from '../models/selection.model';
import { Selection } from '../models/selection.model';
/**
* Gets selected uuids from selection object
*/
export function getSelectedUuids(selection: UnparsedSelection): string[] {
export function getSelectedUuids(selection: Selection): string[] {
const root = selection?.linked_map?.delegate_map?.root?.arr;
if (!root) {

View file

@ -0,0 +1,8 @@
/**
* Converts a string to camelCase from kebab-case and snake_case
*/
export function toCamelCase(str: string): string {
const clean = str.replace(/^(\$|_|-)_?/, '');
return clean.replace(/(_|-)./g, (x) => x[1].toUpperCase());
}