From 50ceb9858e6594eb2fb4ce6379d8bdbf85e85b58 Mon Sep 17 00:00:00 2001 From: James Nash Date: Fri, 22 Nov 2024 01:01:04 +0000 Subject: [PATCH] Added API documentation for parser-utils --- packages/demos/src/simple-dtcg-parser.ts | 12 +- .../dtcg-parser/src/extract-common-props.ts | 4 +- .../dtcg-parser/src/parse-dtcg-file-data.ts | 4 +- packages/dtcg-parser/src/parse-group.ts | 4 +- packages/dtcg-parser/src/parse-token.ts | 4 +- packages/parser-utils/README.md | 77 +++++++- .../parser-utils/src/extractProperties.ts | 34 +++- .../parser-utils/src/isJsonObject.test.ts | 18 +- packages/parser-utils/src/isJsonObject.ts | 20 ++- packages/parser-utils/src/parseData.ts | 168 +++++++++++++++++- 10 files changed, 309 insertions(+), 36 deletions(-) diff --git a/packages/demos/src/simple-dtcg-parser.ts b/packages/demos/src/simple-dtcg-parser.ts index 538a8005..d688e686 100644 --- a/packages/demos/src/simple-dtcg-parser.ts +++ b/packages/demos/src/simple-dtcg-parser.ts @@ -1,4 +1,4 @@ -import { type JsonObject, parseData } from "@udt/parser-utils"; +import { type PlainObject, parseData } from "@udt/parser-utils"; import { readJsonFile, dtcgDevExampleFile } from "./utils/file.js"; import { getArgs } from "./utils/cli-args.js"; @@ -13,7 +13,7 @@ export interface InheritableProperties { // Test impl. -function isDtcgDesignToken(data: JsonObject): boolean { +function isDtcgDesignToken(data: PlainObject): boolean { return data.$value !== undefined; } @@ -23,9 +23,9 @@ export function parseDtcg(data: unknown) { }; function parseDtcgDesignToken( - data: JsonObject, + data: PlainObject, path: string[], - contextFromParent?: JsonObject + contextFromParent?: PlainObject ) { console.log( `Got token ${path.join(".")}, with data: `, @@ -36,9 +36,9 @@ export function parseDtcg(data: unknown) { } function parseDtcgGroup( - data: JsonObject, + data: PlainObject, path: string[], - contextFromParent?: JsonObject + contextFromParent?: PlainObject ) { if (path.length === 0) { console.log( diff --git a/packages/dtcg-parser/src/extract-common-props.ts b/packages/dtcg-parser/src/extract-common-props.ts index c62a1384..728a9b6b 100644 --- a/packages/dtcg-parser/src/extract-common-props.ts +++ b/packages/dtcg-parser/src/extract-common-props.ts @@ -1,5 +1,5 @@ import { isValidType, type TOMNodeCommonConstructorProps } from "@udt/tom"; -import { isJsonObject } from "@udt/parser-utils"; +import { isPlainObject } from "@udt/parser-utils"; export interface ExtractedCommonProps { commonProps: TOMNodeCommonConstructorProps; @@ -13,7 +13,7 @@ export function extractCommonProps(dtcgData: any): ExtractedCommonProps { commonProps: { description: typeof $description === "string" ? $description : undefined, type: isValidType($type) ? $type : undefined, - extensions: isJsonObject($extensions) ? $extensions : undefined, + extensions: isPlainObject($extensions) ? $extensions : undefined, }, rest, }; diff --git a/packages/dtcg-parser/src/parse-dtcg-file-data.ts b/packages/dtcg-parser/src/parse-dtcg-file-data.ts index 8410d380..b27fdc27 100644 --- a/packages/dtcg-parser/src/parse-dtcg-file-data.ts +++ b/packages/dtcg-parser/src/parse-dtcg-file-data.ts @@ -1,4 +1,4 @@ -import { type JsonObject, parseData } from "@udt/parser-utils"; +import { type PlainObject, parseData } from "@udt/parser-utils"; import { RootGroup } from "@udt/tom"; import { parseGroup } from "./parse-group.js"; import { parseToken } from "./parse-token.js"; @@ -9,7 +9,7 @@ import { parseToken } from "./parse-token.js"; * @param data * @returns */ -function isDesignTokenData(data: JsonObject): boolean { +function isDesignTokenData(data: PlainObject): boolean { return data.$value !== undefined; } diff --git a/packages/dtcg-parser/src/parse-group.ts b/packages/dtcg-parser/src/parse-group.ts index fc8150d1..1503e6ff 100644 --- a/packages/dtcg-parser/src/parse-group.ts +++ b/packages/dtcg-parser/src/parse-group.ts @@ -1,9 +1,9 @@ import { type DesignToken, Group, RootGroup } from "@udt/tom"; -import { type JsonObject, type ParseGroupResult } from "@udt/parser-utils"; +import { type PlainObject, type ParseGroupResult } from "@udt/parser-utils"; import { extractCommonProps } from "./extract-common-props.js"; export function parseGroup( - groupProps: JsonObject, + groupProps: PlainObject, path: string[] ): ParseGroupResult { const { commonProps, rest } = extractCommonProps(groupProps); diff --git a/packages/dtcg-parser/src/parse-token.ts b/packages/dtcg-parser/src/parse-token.ts index 1623cd13..112cd255 100644 --- a/packages/dtcg-parser/src/parse-token.ts +++ b/packages/dtcg-parser/src/parse-token.ts @@ -7,10 +7,10 @@ import { import { extractCommonProps } from "./extract-common-props.js"; import { parseValue } from "./values/parse-value.js"; import { isReferenceValue, makeReference } from "./values/reference.js"; -import { type JsonObject } from "@udt/parser-utils"; +import { type PlainObject } from "@udt/parser-utils"; export function parseToken( - tokenProps: JsonObject, + tokenProps: PlainObject, path: string[] ): DesignToken { const name = path[path.length - 1]; diff --git a/packages/parser-utils/README.md b/packages/parser-utils/README.md index a1ae8ab2..2f6a2648 100644 --- a/packages/parser-utils/README.md +++ b/packages/parser-utils/README.md @@ -1,3 +1,78 @@ # Design Token Parser Utilities -Low-level logic and utilities for parsing DTCG and DTCG-like files. Used by `@udt/dtcg-parser` to parse DTCG to TOM, but also suitable for building libraries or tools that parse DTCG(-like) token files to other representations, or need to traverse tokens and/or groups. +Low-level logic and utilities to help build parsers for DTCG and DTCG-like files. Will be used by [`@udt/dtcg-parser`](../dtcg-parser/) to parse DTCG to a Token Object Model (TOM), but also suitable for building libraries or tools that parse DTCG(-like) files to other representations, or need to traverse tokens and/or groups. + +## Usage + +```js +import { parseData } from "@udt/parser-utils"; + +// Given a nested object structure representing groups +// and design tokens (e.g. the result of reading a DTCG file +// and running it through JSON.parse())... +const fileData = { /* ... */ }; + +// ...parseData() will traverse through it and pass the +// relevant properties of each group and design token +// object it encounters to the functions you provide in +// the config: +const parsedData = parseData(fileData, { + /* Parser config */ + + // A function that checks whether an object is + // a design token or not (if not, it is assumed + // to be a group). + // + // E.g. for DTCG data, this could check for the + // presence of a $value property. + isDesignTokenData: (data) => { + /* ... */ + return /* true for tokens, false otherwise */; + }, + + // Array of strings and/or RegExp which match + // properties of group objects that are NOT + // names of child design tokens or groups. + // + // E.g. for DTCG data, this could be a RegEx + // like /^$/ which would match all $-prefixed + // format properties + groupPropsToExtract: [ /* ... */ ]; + + // Function which is called for each group data object + // that is encountered. + // + // Is given the extracted properties of that group and its + // path, and should parse that data into whatever structure + // is desired. + parseGroupData: (data, path, contextFromParent) => { + /* ... */ + return { + group: parsedGroup, + + // optional: + addChild: (childName, childGroupOrToken) => { /*... */ }, + + // optional: + contextForChildren: /* anything you like */, + } + }, + + // 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; + }, +}); +``` + +Note, this package contains TypeScript typings, which are annotated with doc blocks. Please refer to those for more +detail about the parameters and return values of the config +functions. Many popular IDEs (e.g. VSCode) will surface that +info via auto-completions and tooltips as you code, even if +you're writing plain JavaScript. diff --git a/packages/parser-utils/src/extractProperties.ts b/packages/parser-utils/src/extractProperties.ts index be0fe9b4..6cb5d80d 100644 --- a/packages/parser-utils/src/extractProperties.ts +++ b/packages/parser-utils/src/extractProperties.ts @@ -1,10 +1,36 @@ -import { type JsonObject } from "./isJsonObject.js"; +import { type PlainObject } from "./isJsonObject.js"; +/** + * Extracts specfied properties and their values from + * an object. + * + * A bit like destructuring, except that you can also use + * regular expressions to match the properties you want to + * extract. + * + * @param object The plain object from which to extract + * properties. + * @param propsToExtract An array of names, and/or + * regular expressions that match the + * names of the properties to exract. + * @returns The object containing the extracted properties + * and their values, and an array of all property + * names of the input object that were not extracted. + */ export function extractProperties( - object: JsonObject, + object: PlainObject, propsToExtract: (string | RegExp)[] ): { - extracted: JsonObject; + /** + * Object containg the extract properties + * and their respective values. + */ + extracted: PlainObject; + + /** + * Array of property names of the input + * object that were not extracted. + */ remainingProps: string[]; } { const propNamesToExtract = propsToExtract.filter( @@ -14,7 +40,7 @@ export function extractProperties( (prop) => prop instanceof RegExp ); - const extracted: JsonObject = {}; + const extracted: PlainObject = {}; const remainingProps: string[] = []; Object.getOwnPropertyNames(object).forEach((prop) => { if ( diff --git a/packages/parser-utils/src/isJsonObject.test.ts b/packages/parser-utils/src/isJsonObject.test.ts index 4289eeae..a23580e4 100644 --- a/packages/parser-utils/src/isJsonObject.test.ts +++ b/packages/parser-utils/src/isJsonObject.test.ts @@ -1,32 +1,32 @@ import { describe, it, expect } from "vitest"; -import { isJsonObject } from "./isJsonObject.js"; +import { isPlainObject } from "./isJsonObject.js"; -describe("isJsonObject()", () => { +describe("isPlainObject()", () => { it("Returns true for a genuine object", () => { - expect(isJsonObject({ foo: "bar" })).toBe(true); + expect(isPlainObject({ foo: "bar" })).toBe(true); }); it("Returns false for null", () => { - expect(isJsonObject(null)).toBe(false); + expect(isPlainObject(null)).toBe(false); }); it("Returns false for an array", () => { - expect(isJsonObject([1, 2, 3])).toBe(false); + expect(isPlainObject([1, 2, 3])).toBe(false); }); it("Returns false for undefined", () => { - expect(isJsonObject(undefined)).toBe(false); + expect(isPlainObject(undefined)).toBe(false); }); it("Returns false for a function", () => { - expect(isJsonObject(function () {})).toBe(false); + expect(isPlainObject(function () {})).toBe(false); }); it("Returns false for a boolean", () => { - expect(isJsonObject(true)).toBe(false); + expect(isPlainObject(true)).toBe(false); }); it("Returns false for a number", () => { - expect(isJsonObject(42)).toBe(false); + expect(isPlainObject(42)).toBe(false); }); }); diff --git a/packages/parser-utils/src/isJsonObject.ts b/packages/parser-utils/src/isJsonObject.ts index 15744194..f00bcffd 100644 --- a/packages/parser-utils/src/isJsonObject.ts +++ b/packages/parser-utils/src/isJsonObject.ts @@ -1,5 +1,19 @@ -export type JsonObject = Record; +/** + * A plain object. + * + * I.e. `{ ... }`, and not an array or `null`, which + * JavaScript's `typeof` operator would also return + * `"object"` for. + */ +export type PlainObject = Record; -export function isJsonObject(data: unknown): data is JsonObject { - return typeof data === "object" && data !== null && !Array.isArray(data); +/** + * Checks whether a value is a plain object. + * + * @param value The value to check. + * + * @returns `true` if it is a plain object, `false` otherwise. + */ +export function isPlainObject(value: unknown): value is PlainObject { + return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/packages/parser-utils/src/parseData.ts b/packages/parser-utils/src/parseData.ts index 926a6cc3..9f240d95 100644 --- a/packages/parser-utils/src/parseData.ts +++ b/packages/parser-utils/src/parseData.ts @@ -1,40 +1,163 @@ -import { isJsonObject, type JsonObject } from "./isJsonObject.js"; +import { isPlainObject, type PlainObject } from "./isJsonObject.js"; import { extractProperties } from "./extractProperties.js"; -export type IsDesignTokenDataFn = (data: JsonObject) => boolean; +/** + * A function that checks whether an object is a design token + * or not (if not, it is assumed to be a group). + * + * E.g. for DTCG data, this could check for the presence of a + * `$value` property. + * + * @param data A plain object (guaranteed not to `null` or an + * array) + * + * @returns `true` if `data` is design token data, `false` if + * it is group data. + */ +export type IsDesignTokenDataFn = (data: PlainObject) => boolean; +/** + * A function that parses design token data. + * + * @param data A plain object containing design token data + * (guaranteed not to be `null` or an array) + * @param path The path to the design token data. + * @param contextFromParent Context data (if any) that was + * returned by the `parseGroupData()` call that + * parsed the group containing this design token. + * + * @returns The parsed representation of the design token. + * May be `undefined` if there is no useful result + * to return from `parseData()` - e.g. if just + * logging design token info or something like that. + */ export type ParseDesignTokenDataFn = ( - data: JsonObject, + data: PlainObject, path: string[], contextFromParent?: T ) => ParsedDesignToken; +/** + * A function that adds a parsed group or design token + * as a child of a parsed group. + * + * @param name The name of the child group or design token + * @param child The group or desing token to add + */ export type AddChildFn = ( name: string, child: ParsedGroup | ParsedDesignToken ) => void; +/** + * The return value of a `ParseGroupDataFn`. + */ 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. + */ 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; + + /** + * Optional context data to be passed into the + * `parseGroupData()` and `parseDesignTokenData()` calls + * for any nested group or design token data. + * + * Useful if those functions need access to some data from + * higher up in the original data structure. For example, if + * parsing DTCG data, this could be used to pass inherited + * properties like `$type` down. + */ contextForChildren?: T; } +/** + * A function that parses group data. + * + * @param data A plain object containing group data + * (guaranteed not to be `null` or an array) + * @param path The path to the group data. + * @param contextFromParent Context data (if any) that was + * returned by the `parseGroupData()` call that + * 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. + */ export type ParseGroupDataFn = ( - data: JsonObject, + data: PlainObject, path: string[], contextFromParent?: T ) => ParseGroupResult; export interface ParserConfig { + /** + * A function that determines whether an object in the input + * data is a design token or a group. + */ isDesignTokenData: IsDesignTokenDataFn; + + /** + * Array of strings and/or RegExp which match + * properties of group objects that are NOT + * names of child design tokens or groups. + * + * E.g. for DTCG data, this could be a RegEx + * like /^$/ which would match all $-prefixed + * format properties + */ groupPropsToExtract: (string | RegExp)[]; + + /** + * Function which is called for each group data object + * that is encountered. + * + * Is given the extracted properties of that group and its + * path, and should parse that data into whatever structure + * is desired. + */ parseGroupData: ParseGroupDataFn; + + /** + * 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: ParseDesignTokenDataFn; } +/** + * Thrown when `parseData()` encounters group or design token + * data that is not a plain object. + */ export class InvalidDataError extends Error { + /** + * Path to the value that is not a plain object + */ public path: string[]; + + /** + * The offending value. + */ public data: unknown; constructor(path: string[], data: unknown) { @@ -49,6 +172,21 @@ export class InvalidDataError extends Error { } } +/** + * The internal data parsing implementation. + * + * Recursively calls itself for nested group and + * design token data. + * + * @param data The input data to traverse and parse + * @param config Parser config + * @param contextFromParent Context data passed in from + * parent calls to this function. + * @param path The path to the input data + * @param addToParent A function to add the parsed data + * to the parent group. + * @returns The parsed design token or group. + */ function parseDataImpl( data: unknown, config: ParserConfig, @@ -56,7 +194,7 @@ function parseDataImpl( path: string[] = [], addToParent?: AddChildFn ): ParsedDesignToken | ParsedGroup { - if (!isJsonObject(data)) { + if (!isPlainObject(data)) { throw new InvalidDataError(path, data); } @@ -104,6 +242,26 @@ function parseDataImpl( return groupOrToken; } +/** + * Parses a nested object structure representing groups + * and design tokens, such as the data obtained by reading + * and JSON-parsing a DTCG file. + * + * It will recursively traverse the input data (depth first) + * and, using the functions provided in the config: + * + * 1. Check if the object is design token or group data + * 2. Pass that data to the parsed or processed by the + * relevant function + * 3. Return the outermost parsed group or design token + * + * @param data The input data to traverse and parse + * @param config Configuration for this parser + * @param contextFromParent Optional context data to + * pass into the top-most design token + * or group parser function call. + * @returns The outermost parsed group or design token + */ export function parseData( data: unknown, config: ParserConfig,