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:
parent
eb715ddb7c
commit
efca5d4d1a
23 changed files with 676 additions and 466 deletions
|
@ -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);
|
||||
```
|
||||
|
|
|
@ -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';
|
||||
|
|
22
libs/plugins-data-parser/src/lib/helpers/file.helper.ts
Normal file
22
libs/plugins-data-parser/src/lib/helpers/file.helper.ts
Normal 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;
|
||||
}
|
||||
}
|
3
libs/plugins-data-parser/src/lib/helpers/index.ts
Normal file
3
libs/plugins-data-parser/src/lib/helpers/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './file.helper';
|
||||
export * from './page.helper';
|
||||
export * from './selection.helper';
|
97
libs/plugins-data-parser/src/lib/helpers/page.helper.ts
Normal file
97
libs/plugins-data-parser/src/lib/helpers/page.helper.ts
Normal 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;
|
||||
}
|
||||
}
|
15
libs/plugins-data-parser/src/lib/helpers/selection.helper.ts
Normal file
15
libs/plugins-data-parser/src/lib/helpers/selection.helper.ts
Normal 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[]) || [];
|
||||
}
|
||||
}
|
181
libs/plugins-data-parser/src/lib/models/extmap.model.ts
Normal file
181
libs/plugins-data-parser/src/lib/models/extmap.model.ts
Normal 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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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';
|
||||
|
|
44
libs/plugins-data-parser/src/lib/models/object.model.ts
Normal file
44
libs/plugins-data-parser/src/lib/models/object.model.ts
Normal 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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export interface UnparsedSelection extends CljValues {
|
||||
export interface Selection extends CljValues {
|
||||
linked_map: SelLinkedMap;
|
||||
}
|
||||
|
||||
|
|
88
libs/plugins-data-parser/src/lib/utils/arr.util.ts
Normal file
88
libs/plugins-data-parser/src/lib/utils/arr.util.ts
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
8
libs/plugins-data-parser/src/lib/utils/string.util.ts
Normal file
8
libs/plugins-data-parser/src/lib/utils/string.util.ts
Normal 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());
|
||||
}
|
Loading…
Reference in a new issue