diff --git a/.changeset/loud-dragons-provide.md b/.changeset/loud-dragons-provide.md new file mode 100644 index 0000000..62ca4b7 --- /dev/null +++ b/.changeset/loud-dragons-provide.md @@ -0,0 +1,5 @@ +--- +"@udt/parser-utils": minor +--- + +BREAKING CHANGE: `ParseGroupDataFn` can no longer return an `addChild` function. Instead, you should now provide an `addChildToGroup` function in your parser config. diff --git a/.changeset/slow-insects-marry.md b/.changeset/slow-insects-marry.md new file mode 100644 index 0000000..04bc3bf --- /dev/null +++ b/.changeset/slow-insects-marry.md @@ -0,0 +1,5 @@ +--- +"@udt/parser-utils": minor +--- + +BREAKING CHANGE: `parseData()` may now return `undefined` (happens if no `parseGroupData` function is set in the config). diff --git a/.changeset/tidy-islands-punch.md b/.changeset/tidy-islands-punch.md new file mode 100644 index 0000000..99323df --- /dev/null +++ b/.changeset/tidy-islands-punch.md @@ -0,0 +1,5 @@ +--- +"@udt/parser-utils": minor +--- + +BREAKING CHANGE: `extractProperties()` now returns an object containing unextract properties and their values, instead of an array of unextracted property names. diff --git a/packages/demos/src/simple-dtcg-parser.ts b/packages/demos/src/simple-dtcg-parser.ts index d688e68..e066ead 100644 --- a/packages/demos/src/simple-dtcg-parser.ts +++ b/packages/demos/src/simple-dtcg-parser.ts @@ -71,7 +71,6 @@ export function parseDtcg(data: unknown) { } return { - group: undefined, contextForChildren, }; } diff --git a/packages/dtcg-parser/src/parse-dtcg-file-data.ts b/packages/dtcg-parser/src/parse-dtcg-file-data.ts index b27fdc2..287ebd4 100644 --- a/packages/dtcg-parser/src/parse-dtcg-file-data.ts +++ b/packages/dtcg-parser/src/parse-dtcg-file-data.ts @@ -21,6 +21,9 @@ export function parseDtcgFileData(dtcgData: unknown): RootGroup { groupPropsToExtract: [dtcgPropRegex], parseGroupData: parseGroup, parseDesignTokenData: parseToken, + addChildToGroup(group, _name, child) { + group.addChild(child); + }, }); if (!(result instanceof RootGroup)) { diff --git a/packages/dtcg-parser/src/parse-group.ts b/packages/dtcg-parser/src/parse-group.ts index 1503e6f..c66a24d 100644 --- a/packages/dtcg-parser/src/parse-group.ts +++ b/packages/dtcg-parser/src/parse-group.ts @@ -1,11 +1,11 @@ -import { type DesignToken, Group, RootGroup } from "@udt/tom"; +import { Group, RootGroup } from "@udt/tom"; import { type PlainObject, type ParseGroupResult } from "@udt/parser-utils"; import { extractCommonProps } from "./extract-common-props.js"; export function parseGroup( groupProps: PlainObject, path: string[] -): ParseGroupResult { +): ParseGroupResult { const { commonProps, rest } = extractCommonProps(groupProps); if (Object.keys(rest).length > 0) { @@ -22,8 +22,5 @@ export function parseGroup( return { group, - addChild(_name, child: Group | DesignToken) { - group.addChild(child); - }, }; } diff --git a/packages/parser-utils/README.md b/packages/parser-utils/README.md index 2f6a264..22cb35b 100644 --- a/packages/parser-utils/README.md +++ b/packages/parser-utils/README.md @@ -39,7 +39,18 @@ const parsedData = parseData(fileData, { // format properties groupPropsToExtract: [ /* ... */ ]; - // Function which is called for each group data object + // Function which is called for each design token + // data object that is encountered. + // + // Is given the design token data and its path, and + // should parse that data into whatever structure is + // desired. + parseDesignTokenData: (data, path, contextFromParent) => { + /* ... */ + return parsedDesignToken; + }, + + // OPTIONAL function which is called for each group data object // that is encountered. // // Is given the extracted properties of that group and its @@ -50,24 +61,17 @@ const parsedData = parseData(fileData, { return { group: parsedGroup, - // optional: - addChild: (childName, childGroupOrToken) => { /*... */ }, - - // optional: + // OPTIONAL: contextForChildren: /* anything you like */, } }, - // Function which is called for each design token - // data object that is encountered. + // OPTIONAL function which is called for each design token and/or + // nested group found within a group. // - // Is given the design token data and its path, and - // should parse that data into whatever structure is - // desired. - parseDesignTokenData: (data, path, contextFromParent) => { - /* ... */ - return parsedDesignToken; - }, + // Useful if your parsed groups and design tokens need to be assembled into + // a tree structure. + addChildToGroup: (parsedParentGroup, childName, parsedChildGroupOrToken) => { /*... */ }, }); ``` diff --git a/packages/parser-utils/src/extractProperties.test.ts b/packages/parser-utils/src/extractProperties.test.ts index 184aa74..46ab8b0 100644 --- a/packages/parser-utils/src/extractProperties.test.ts +++ b/packages/parser-utils/src/extractProperties.test.ts @@ -18,12 +18,12 @@ describe("extractProperties()", () => { expect(extractResult.extracted).toStrictEqual({ bar: 13, baz: 666 }); }); - it("returns keys of non-extracted properties", () => { + it("returns non-extracted properties", () => { const extractResult = extractProperties( { foo: 42, bar: 13, baz: 666, quux: 0 }, ["foo", "baz"] ); - expect(extractResult.remainingProps).toStrictEqual(["bar", "quux"]); + expect(extractResult.rest).toStrictEqual({ bar: 13, quux: 0 }); }); it("ignores props to extract that are not present in input object", () => { diff --git a/packages/parser-utils/src/extractProperties.ts b/packages/parser-utils/src/extractProperties.ts index 6cb5d80..f160b40 100644 --- a/packages/parser-utils/src/extractProperties.ts +++ b/packages/parser-utils/src/extractProperties.ts @@ -19,7 +19,7 @@ import { type PlainObject } from "./isJsonObject.js"; */ export function extractProperties( object: PlainObject, - propsToExtract: (string | RegExp)[] + propsToExtract: readonly (string | RegExp)[] ): { /** * Object containg the extract properties @@ -28,10 +28,10 @@ export function extractProperties( extracted: PlainObject; /** - * Array of property names of the input - * object that were not extracted. + * Object containing the remaining, unextracted + * properties and their respective values. */ - remainingProps: string[]; + rest: PlainObject; } { const propNamesToExtract = propsToExtract.filter( (prop) => typeof prop === "string" @@ -41,7 +41,7 @@ export function extractProperties( ); const extracted: PlainObject = {}; - const remainingProps: string[] = []; + const rest: PlainObject = {}; Object.getOwnPropertyNames(object).forEach((prop) => { if ( propNamesToExtract.some( @@ -53,12 +53,12 @@ export function extractProperties( ) { extracted[prop] = object[prop]; } else { - remainingProps.push(prop); + rest[prop] = object[prop]; } }); return { extracted, - remainingProps, + rest, }; } diff --git a/packages/parser-utils/src/index.ts b/packages/parser-utils/src/index.ts index c9fb641..d5abfad 100644 --- a/packages/parser-utils/src/index.ts +++ b/packages/parser-utils/src/index.ts @@ -1,3 +1,5 @@ +/* v8 ignore start */ export * from "./parseData.js"; export * from "./isJsonObject.js"; export * from "./extractProperties.js"; +/* v8 ignore end */ diff --git a/packages/parser-utils/src/parseData.test.ts b/packages/parser-utils/src/parseData.test.ts index 7fcf06c..aebbcaf 100644 --- a/packages/parser-utils/src/parseData.test.ts +++ b/packages/parser-utils/src/parseData.test.ts @@ -19,10 +19,6 @@ interface TestDesignToken { interface TestParentContext { dummyData?: number; - - // Used to pass in a mock addChild() function for - // mockParseGroupData() to use for testing - addChild?: AddChildFn; } interface ParseDataCall { @@ -53,7 +49,7 @@ const mockIsDesignTokenData = vi.fn( ); const mockParseGroupData = vi.fn< - ParseGroupDataFn + ParseGroupDataFn >((_data, path, contextFromParent) => { parseDataCalls.push({ type: "group", @@ -65,12 +61,18 @@ const mockParseGroupData = vi.fn< }, // pass through contextFromParent contextForChildren: contextFromParent, - - // pass through addChild - addChild: contextFromParent?.addChild, }; }); +const mockAddChildToGroup = vi.fn>( + (_parent, name, _child) => { + parseDataCalls.push({ + type: "addChild", + name, + }); + } +); + const mockParseDesignTokenData = vi.fn< ParseDesignTokenDataFn >((_data, path, _contextFromParent) => { @@ -81,20 +83,23 @@ const mockParseDesignTokenData = vi.fn< return result; }); +const defaultParserConfig: ParserConfig< + TestDesignToken, + TestGroup, + TestParentContext +> = { + isDesignTokenData: mockIsDesignTokenData, + groupPropsToExtract: [], + parseGroupData: mockParseGroupData, + parseDesignTokenData: mockParseDesignTokenData, +}; + describe("parseData()", () => { - const parserConfig: ParserConfig< - TestDesignToken, - TestGroup, - TestParentContext - > = { - isDesignTokenData: mockIsDesignTokenData, - groupPropsToExtract: [], - parseGroupData: mockParseGroupData, - parseDesignTokenData: mockParseDesignTokenData, - }; + let parserConfig: ParserConfig; beforeEach(() => { // Reset stuff + parserConfig = defaultParserConfig; parseDataCalls = []; parserConfig.groupPropsToExtract = []; mockIsDesignTokenData.mockClear(); @@ -103,14 +108,14 @@ describe("parseData()", () => { }); describe("parsing an empty group object", () => { - let parsedGroupOrToken: TestGroup | TestDesignToken; + let parsedGroupOrToken: TestGroup | TestDesignToken | undefined; beforeEach(() => { parsedGroupOrToken = parseData({}, parserConfig); }); it("returns a group", () => { - expect(parsedGroupOrToken.type).toBe("group"); + expect(parsedGroupOrToken?.type).toBe("group"); }); it("calls isDesignTokenData function once", () => { @@ -141,14 +146,14 @@ describe("parseData()", () => { stuff: 123, notAGroup: {}, }; - let parsedGroupOrToken: TestGroup | TestDesignToken; + let parsedGroupOrToken: TestGroup | TestDesignToken | undefined; beforeEach(() => { parsedGroupOrToken = parseData(testTokenData, parserConfig); }); it("returns a design token", () => { - expect(parsedGroupOrToken.type).toBe("token"); + expect(parsedGroupOrToken?.type).toBe("token"); }); it("does not call parseGroupData function", () => { @@ -320,32 +325,24 @@ describe("parseData()", () => { }); }); - describe("using addChild functions", () => { + describe("using addChildToGroup function", () => { const testData = { tokenA: { value: 1 }, tokenB: { value: 2 }, groupC: { tokenX: { value: 99 }, tokenY: { value: 100 } }, }; - const mockAddChild = vi.fn>( - (name, _child) => { - parseDataCalls.push({ - type: "addChild", - name, - }); - } - ); - const testContext: TestParentContext = { addChild: mockAddChild }; beforeEach(() => { - mockAddChild.mockClear(); - parseData(testData, parserConfig, testContext); + mockAddChildToGroup.mockClear(); + parserConfig.addChildToGroup = mockAddChildToGroup; + parseData(testData, parserConfig); }); - it("calls addChild for every child of every group", () => { + it("calls addChildToGroup for every child of every group", () => { // Root group contains 3 children: "tokenA", "tokenB" and "groupC" // "groupC" contains 2 children: "tokenX" and "tokenY" // 3 + 2 = 5 - expect(mockAddChild).toHaveBeenCalledTimes(5); + expect(mockAddChildToGroup).toHaveBeenCalledTimes(5); }); it("adds a nested group to its parent before parsing its children", () => { diff --git a/packages/parser-utils/src/parseData.ts b/packages/parser-utils/src/parseData.ts index 9f240d9..a2b2b46 100644 --- a/packages/parser-utils/src/parseData.ts +++ b/packages/parser-utils/src/parseData.ts @@ -39,12 +39,14 @@ export type ParseDesignTokenDataFn = ( /** * A function that adds a parsed group or design token - * as a child of a parsed group. + * as a child of the given parsed group. * + * @param parent The parent group to add a child to * @param name The name of the child group or design token - * @param child The group or desing token to add + * @param child The group or design token to add */ export type AddChildFn = ( + parent: ParsedGroup, name: string, child: ParsedGroup | ParsedDesignToken ) => void; @@ -52,25 +54,15 @@ export type AddChildFn = ( /** * The return value of a `ParseGroupDataFn`. */ -export interface ParseGroupResult { +export interface ParseGroupResult { /** * The parsed representation of the group. * - * May be `undefined` if there is no useful result - * to return from `parseData()` - e.g. if just - * logging group info or something like that. + * May be omitted if there is no useful result to + * return and we only need to pass along context + * data. */ - group: ParsedGroup; - - /** - * Optional function that will add other parsed groups - * or design tokens as children of this parsed group. - * - * Intended for cases where the parsed representation - * of a group needs to contain its children. If not - * needed, this property can be omitted. - */ - addChild?: AddChildFn; + group?: ParsedGroup; /** * Optional context data to be passed into the @@ -96,15 +88,14 @@ export interface ParseGroupResult { * parsed the group containing this group. * * @returns The parsed representation of the group and, - * optionally, a function to add child groups or - * design tokens to it and some context data to - * pass down when child data is parsed. + * optionally, some context data to pass down + * when child data is parsed. */ -export type ParseGroupDataFn = ( +export type ParseGroupDataFn = ( data: PlainObject, path: string[], contextFromParent?: T -) => ParseGroupResult; +) => ParseGroupResult; export interface ParserConfig { /** @@ -132,7 +123,7 @@ export interface ParserConfig { * path, and should parse that data into whatever structure * is desired. */ - parseGroupData: ParseGroupDataFn; + parseGroupData?: ParseGroupDataFn; /** * Function which is called for each design token @@ -143,6 +134,16 @@ export interface ParserConfig { * desired. */ parseDesignTokenData: ParseDesignTokenDataFn; + + /** + * Optional function that will add parsed groups + * or design tokens as children of another parsed group. + * + * Intended for cases where the parsed representation + * of a group needs to contain its children. If not + * needed, this property can be omitted. + */ + addChildToGroup?: AddChildFn; } /** @@ -192,8 +193,8 @@ function parseDataImpl( config: ParserConfig, contextFromParent?: T, path: string[] = [], - addToParent?: AddChildFn -): ParsedDesignToken | ParsedGroup { + parentGroup?: ParsedGroup +): ParsedDesignToken | ParsedGroup | undefined { if (!isPlainObject(data)) { throw new InvalidDataError(path, data); } @@ -203,38 +204,46 @@ function parseDataImpl( groupPropsToExtract, parseGroupData, parseDesignTokenData, + addChildToGroup, } = config; - let groupOrToken: ParsedGroup | ParsedDesignToken; + let groupOrToken: ParsedGroup | ParsedDesignToken | undefined = undefined; if (isDesignTokenData(data)) { // looks like a token groupOrToken = parseDesignTokenData(data, path, contextFromParent); - if (addToParent && path.length > 0) { - addToParent(path[path.length - 1], groupOrToken); + if (addChildToGroup && path.length > 0 && parentGroup !== undefined) { + addChildToGroup(parentGroup, path[path.length - 1], groupOrToken); } } else { // must be a group - const { extracted: groupData, remainingProps: childNames } = - extractProperties(data, groupPropsToExtract); - const { group, addChild, contextForChildren } = parseGroupData( - groupData, - path, - contextFromParent + const { extracted: groupData, rest: children } = extractProperties( + data, + groupPropsToExtract ); - groupOrToken = group; + let contextForChildren: T | undefined; + if (parseGroupData) { + const parseResult = parseGroupData(groupData, path, contextFromParent); + contextForChildren = parseResult.contextForChildren; + groupOrToken = parseResult.group; + } - if (addToParent && path.length > 0) { - addToParent(path[path.length - 1], groupOrToken); + if ( + addChildToGroup && + path.length > 0 && + parentGroup !== undefined && + groupOrToken !== undefined + ) { + addChildToGroup(parentGroup, path[path.length - 1], groupOrToken); } - for (const childName of childNames) { + for (const childName in children) { parseDataImpl( - data[childName], + children[childName], config, contextForChildren, [...path, childName], - addChild + groupOrToken ); } } @@ -266,6 +275,6 @@ export function parseData( data: unknown, config: ParserConfig, contextFromParent?: T -): ParsedDesignToken | ParsedGroup { +): ParsedDesignToken | ParsedGroup | undefined { return parseDataImpl(data, config, contextFromParent); }