diff --git a/libs/plugins-data-parser/README.md b/libs/plugins-data-parser/README.md index 2927dfc4..2d57e01c 100644 --- a/libs/plugins-data-parser/README.md +++ b/libs/plugins-data-parser/README.md @@ -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 +const fileHelper = new FileHelper(); +fileHelper.setData(data); +``` + +or + +```ts +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 -import { parse, ParsedFile } from 'plugins-parser'; +const pageHelper = new PageHelper(); +pageHelper.setData(data); +``` -[...] +or -const parsedFile: ParsedFile = parse(file); +```ts +const pageHelper = new PageHelper(data); +``` -const color = parsedFile.data.colors?.[0]; +- `getCleanData()` -/** color: - * { - * "path": "Background", - * "color": "#1c1b1f", - * "fileId": "f13ed095-e13f-808c-8002-2830d45911f7", - * "name": "On Background", - * "opacity": 1, - * "id": "136eece0-40ab-8002-8002-296771ace070" - * } - */ +Gets the cleaned up data. It deletes useless properties and and transforms the remaining ones to camelCase. +example: + +```ts +const clean = pageHelper.getCleanData(); ``` -- `getSelectedUuids()` +- `getObjectsArray()` + +Returns the objects array, which can contain heavily nested arrays with objects data. + +example: ```ts -import { getSelectedUuids, UnparsedSelection } from 'plugins-parser'; +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 -const selection: UnparsedSelection = { [...] }; +#### Selection Helper functions -const ids: string[] = getSelectedUuids(selection); => +- `static getUuids(selection: Selection)` -/** ids: - * [ - * "4fa12080-0d58-80a3-8002-3a2344356e7c", - * "d9a61226-8431-8080-8002-5aea35acc724" - * ] - */ +Returns the selected items in an array. +example: + +```ts +const ids = SelectionHelper.getUuids(selection); ``` diff --git a/libs/plugins-data-parser/src/index.ts b/libs/plugins-data-parser/src/index.ts index 7964e991..44affafb 100644 --- a/libs/plugins-data-parser/src/index.ts +++ b/libs/plugins-data-parser/src/index.ts @@ -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'; diff --git a/libs/plugins-data-parser/src/lib/helpers/file.helper.ts b/libs/plugins-data-parser/src/lib/helpers/file.helper.ts new file mode 100644 index 00000000..53aafdc2 --- /dev/null +++ b/libs/plugins-data-parser/src/lib/helpers/file.helper.ts @@ -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; + } +} diff --git a/libs/plugins-data-parser/src/lib/helpers/index.ts b/libs/plugins-data-parser/src/lib/helpers/index.ts new file mode 100644 index 00000000..80357ed1 --- /dev/null +++ b/libs/plugins-data-parser/src/lib/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './file.helper'; +export * from './page.helper'; +export * from './selection.helper'; diff --git a/libs/plugins-data-parser/src/lib/helpers/page.helper.ts b/libs/plugins-data-parser/src/lib/helpers/page.helper.ts new file mode 100644 index 00000000..0cec4481 --- /dev/null +++ b/libs/plugins-data-parser/src/lib/helpers/page.helper.ts @@ -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 | 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; + 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; + } +} diff --git a/libs/plugins-data-parser/src/lib/helpers/selection.helper.ts b/libs/plugins-data-parser/src/lib/helpers/selection.helper.ts new file mode 100644 index 00000000..ab8b2d1d --- /dev/null +++ b/libs/plugins-data-parser/src/lib/helpers/selection.helper.ts @@ -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[]) || []; + } +} diff --git a/libs/plugins-data-parser/src/lib/models/extmap.model.ts b/libs/plugins-data-parser/src/lib/models/extmap.model.ts new file mode 100644 index 00000000..cc9afd4e --- /dev/null +++ b/libs/plugins-data-parser/src/lib/models/extmap.model.ts @@ -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; + 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; +} diff --git a/libs/plugins-data-parser/src/lib/models/file.model.ts b/libs/plugins-data-parser/src/lib/models/file.model.ts deleted file mode 100644 index 79436f3a..00000000 --- a/libs/plugins-data-parser/src/lib/models/file.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Data, ParsedData } from '.'; - -export type FileDataType = - | 'colors' - | 'typographies' - | 'pages' - | 'media' - | 'pagesIndex' - | 'components'; - -export interface ParsedFile extends Omit { - data: Pick; -} diff --git a/libs/plugins-data-parser/src/lib/models/index.ts b/libs/plugins-data-parser/src/lib/models/index.ts index 15bc0657..88ca0b0a 100644 --- a/libs/plugins-data-parser/src/lib/models/index.ts +++ b/libs/plugins-data-parser/src/lib/models/index.ts @@ -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'; diff --git a/libs/plugins-data-parser/src/lib/models/object.model.ts b/libs/plugins-data-parser/src/lib/models/object.model.ts new file mode 100644 index 00000000..ba3dd3ee --- /dev/null +++ b/libs/plugins-data-parser/src/lib/models/object.model.ts @@ -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; +} diff --git a/libs/plugins-data-parser/src/lib/models/page.model.ts b/libs/plugins-data-parser/src/lib/models/page.model.ts deleted file mode 100644 index 1db1b484..00000000 --- a/libs/plugins-data-parser/src/lib/models/page.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Data, ParsedData } from '.'; - -export type PageDataType = 'options' | 'objects' | 'name' | 'id'; - -export interface ParsedPage extends Omit { - data: Pick; -} diff --git a/libs/plugins-data-parser/src/lib/models/parsed.model.ts b/libs/plugins-data-parser/src/lib/models/parsed.model.ts deleted file mode 100644 index a2d70ed6..00000000 --- a/libs/plugins-data-parser/src/lib/models/parsed.model.ts +++ /dev/null @@ -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; // 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; - 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 { - root: R; - tail: T; -} diff --git a/libs/plugins-data-parser/src/lib/models/selection.model.ts b/libs/plugins-data-parser/src/lib/models/selection.model.ts index 49094de5..c2f602af 100644 --- a/libs/plugins-data-parser/src/lib/models/selection.model.ts +++ b/libs/plugins-data-parser/src/lib/models/selection.model.ts @@ -1,4 +1,4 @@ -export interface UnparsedSelection extends CljValues { +export interface Selection extends CljValues { linked_map: SelLinkedMap; } diff --git a/libs/plugins-data-parser/src/lib/utils/arr.util.ts b/libs/plugins-data-parser/src/lib/utils/arr.util.ts new file mode 100644 index 00000000..105f3b4d --- /dev/null +++ b/libs/plugins-data-parser/src/lib/utils/arr.util.ts @@ -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 | null { + return ( + (Array.isArray(arr) && + arr.reduce( + (result: Record, 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 | null { + if (toArray(arr)) { + return arrToArray(arr); + } else if (toObject(arr)) { + return arrToObject(arr); + } + + return arr; +} diff --git a/libs/plugins-data-parser/src/lib/utils/index.ts b/libs/plugins-data-parser/src/lib/utils/index.ts index d05ab6e7..2fd55c55 100644 --- a/libs/plugins-data-parser/src/lib/utils/index.ts +++ b/libs/plugins-data-parser/src/lib/utils/index.ts @@ -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'; diff --git a/libs/plugins-data-parser/src/lib/models/utils.model.ts b/libs/plugins-data-parser/src/lib/utils/models/util.model.ts similarity index 100% rename from libs/plugins-data-parser/src/lib/models/utils.model.ts rename to libs/plugins-data-parser/src/lib/utils/models/util.model.ts diff --git a/libs/plugins-data-parser/src/lib/utils/object.util.ts b/libs/plugins-data-parser/src/lib/utils/object.util.ts index 3bcf2080..89e92449 100644 --- a/libs/plugins-data-parser/src/lib/utils/object.util.ts +++ b/libs/plugins-data-parser/src/lib/utils/object.util.ts @@ -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).length === 1 && + !!(object as Record)[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); + + 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); + return keys.length === 2 && keys.includes('root') && keys.includes('tail'); + } + + return false; } diff --git a/libs/plugins-data-parser/src/lib/utils/parse-arr.util.ts b/libs/plugins-data-parser/src/lib/utils/parse-arr.util.ts deleted file mode 100644 index 87475801..00000000 --- a/libs/plugins-data-parser/src/lib/utils/parse-arr.util.ts +++ /dev/null @@ -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; - - 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, { ...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']); - } - - 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; -} diff --git a/libs/plugins-data-parser/src/lib/utils/parse-properties.util.ts b/libs/plugins-data-parser/src/lib/utils/parse-properties.util.ts deleted file mode 100644 index 60fe95e6..00000000 --- a/libs/plugins-data-parser/src/lib/utils/parse-properties.util.ts +++ /dev/null @@ -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).length === 1 && - !!(object as Record)[property] - ); - } - - return false; -} - -export function isSingleObjectWithProperties( - object: unknown, - properties: string[] -): boolean { - if (isObject(object)) { - const keys = Object.keys(object as Record); - - 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)[property]; - } - }); - - return result; -} diff --git a/libs/plugins-data-parser/src/lib/utils/parse-state.ts b/libs/plugins-data-parser/src/lib/utils/parse-state.ts index 1a9e6658..57ba6d1f 100644 --- a/libs/plugins-data-parser/src/lib/utils/parse-state.ts +++ b/libs/plugins-data-parser/src/lib/utils/parse-state.ts @@ -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; diff --git a/libs/plugins-data-parser/src/lib/utils/parse.util.ts b/libs/plugins-data-parser/src/lib/utils/parse.util.ts index 25338356..cb590a53 100644 --- a/libs/plugins-data-parser/src/lib/utils/parse.util.ts +++ b/libs/plugins-data-parser/src/lib/utils/parse.util.ts @@ -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)[property]; + } + }); + + return result; +} + +export function parseRootTail(obj: unknown): unknown { + if (isObject(obj) && isRootTail(obj)) { + const { root, tail } = obj as Record; + + 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,70 +64,61 @@ export function cleanObject(obj: unknown): unknown { return Object.keys(obj as Record) .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)[key]; + if (['extmap', '$extmap'].includes(key) && value === null) { + return { ...result }; + } + + return { ...result, [toCamelCase(key)]: cleanObject( (obj as Record)[key] ), - }), - {} - ); + }; + }, {}); } return obj as Record; } -/** - * 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) => parseObject(v)); - - // Flatten nested arrays if necessary - return flattenNestedArrays(parsedArray); + const singleProperties = ['root', 'name', 'uuid', 'guides']; + if (isSingleObjectWithProperties(obj, singleProperties)) { + const parsed = parseSingleProperties( + obj as Record, + singleProperties + ); + return parseObject(parsed); } - // If it's an object with only property 'arr', parse it if (isSingleObjectWithProperty(obj, 'arr')) { - const parsed = parseObjArr(obj as Record); + const parsed = parseArrProperty((obj as Arr).arr); return parseObject(parsed); } - // If it's an object with only properties singleProperties, parse them - const singleProperties = ['root', 'uuid', 'name', 'guides']; - if (isSingleObjectWithProperties(obj, singleProperties)) { - const parsed = parseSingleProperties( - obj as Record, - singleProperties - ); + 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) => parseObject(v)); + } + // If it's an object, parse each property if (isObject(obj)) { - return Object.keys(obj as Record).reduce( - (result, key) => ({ + return Object.keys(obj as Record).reduce((result, key) => { + return { ...result, [key]: parseObject((obj as Record)[key]), - }), - {} - ); + }; + }, {}); } return obj; } - -/** - * Parse object into a more typescript friendly object - */ -export function parse(file: unknown): ParsedData { - return parseObject(cleanObject(file)) as ParsedData; -} diff --git a/libs/plugins-data-parser/src/lib/utils/selected.util.ts b/libs/plugins-data-parser/src/lib/utils/selected.util.ts index f1f89466..14d9dffa 100644 --- a/libs/plugins-data-parser/src/lib/utils/selected.util.ts +++ b/libs/plugins-data-parser/src/lib/utils/selected.util.ts @@ -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) { diff --git a/libs/plugins-data-parser/src/lib/utils/string.util.ts b/libs/plugins-data-parser/src/lib/utils/string.util.ts new file mode 100644 index 00000000..e8e78de0 --- /dev/null +++ b/libs/plugins-data-parser/src/lib/utils/string.util.ts @@ -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()); +}