Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(design): add sorting way based on regexps #2379

Merged
merged 1 commit into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BuilderContext } from '@angular-devkit/architect';
import {
type CssTokenDefinitionRendererOptions,
type CssTokenValueRendererOptions,
type DesignTokenListTransform,
type DesignTokenRendererOptions,
type DesignTokenVariableStructure,
getCssStyleContentUpdater,
Expand All @@ -12,13 +13,15 @@ import {
getSassTokenValueRenderer,
getTokenSorterByName,
getTokenSorterByRef,
getTokenSorterFromRegExpList,
type SassTokenDefinitionRendererOptions,
type SassTokenValueRendererOptions,
type TokenKeyRenderer,
tokenVariableNameSassRenderer
} from '../../../src/public_api';
import type { GenerateStyleSchematicsSchema } from '../schema';
import { resolve } from 'node:path';
import { readFileSync } from 'node:fs';

export const getStyleRendererOptions = (tokenVariableNameRenderer: TokenKeyRenderer | undefined , options: GenerateStyleSchematicsSchema, context: BuilderContext): DesignTokenRendererOptions => {

Expand Down Expand Up @@ -113,14 +116,35 @@ export const getStyleRendererOptions = (tokenVariableNameRenderer: TokenKeyRende

/** Sorting strategy of variables based on selected language */
const tokenListTransforms = ((language) => {
const customSorter: DesignTokenListTransform[] = [];
if (options.sortOrderPatternsFilePath) {
try {
const regExps = (JSON.parse(readFileSync(resolve(context.workspaceRoot, options.sortOrderPatternsFilePath), {encoding: 'utf8'})) as string[])
.map((item) => new RegExp(item.replace(/^\/(.*)\/$/, '$1')));
customSorter.push(getTokenSorterFromRegExpList(regExps));
} catch (err: any) {
if (err?.code === 'ENOENT') {
context.logger.warn(`The specified RegExp file ${options.sortOrderPatternsFilePath} is not found in ${context.workspaceRoot}`);
} else {
context.logger.warn(`Error during the parsing of ${options.sortOrderPatternsFilePath}.`);
if (err instanceof Error) {
context.logger.warn(err.message);
context.logger.debug(err.stack || 'no stack');
} else {
context.logger.debug(JSON.stringify(err, null, 2));
}
}
context.logger.warn(`The ordered list will be ignored.`);
}
}
switch (language) {
case 'scss':
case 'sass': {
return [getTokenSorterByName, getTokenSorterByRef];
return [getTokenSorterByName, ...customSorter, getTokenSorterByRef];
mrednic-1A marked this conversation as resolved.
Show resolved Hide resolved
}
case 'css':
default: {
return [getTokenSorterByName];
return [getTokenSorterByName, ...customSorter];
}
}
})(options.variableType || options.language);
Expand Down
4 changes: 4 additions & 0 deletions packages/@o3r/design/builders/generate-style/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@
"type": "boolean",
"default": false,
"description": "Determine if the builder should fail if a missing Design Token reference is detected"
},
"sortOrderPatternsFilePath": {
"type": "string",
"description": "Path to the JSON file exposing an ordered array of RegExps applied to the token name which will define the priority of the generated variables. (Note: not matching tokens will default to ASC order)"
}
},
"additionalProperties": true,
Expand Down
6 changes: 6 additions & 0 deletions packages/@o3r/design/builders/generate-style/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,10 @@ export interface GenerateStyleSchematicsSchema extends SchematicOptionObject {

/** Path to a template file to apply as default configuration to a Design Token extension */
templateFile?: string | string[];

/**
* Path to the JSON file exposing an ordered array of RegExps applied to the token name which will define the priority of the generated variables.
* Note: not matching tokens will default to ASC order.
*/
sortOrderPatternsFilePath?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import {
} from '../design-token-specification.interface';
import { dirname } from 'node:path';

/** Separator in Token key parts */
export const TOKEN_KEY_SEPARATOR = '.';

const tokenReferenceRegExp = /\{([^}]+)\}/g;
const splitValueNumericRegExp = /^([-+]?[0-9]+[.,]?[0-9]*)\s*([^\s.,;]+)?/;

const getTokenReferenceName = (tokenName: string, parents: string[]) => parents.join('.') + (parents.length ? '.' : '') + tokenName;
const getTokenReferenceName = (tokenName: string, parents: string[]) => parents.join(TOKEN_KEY_SEPARATOR) + (parents.length ? TOKEN_KEY_SEPARATOR : '') + tokenName;
const getExtensions = (nodes: NodeReference[], context: DesignTokenContext | undefined) => {
return nodes.reduce((acc, {tokenNode}, i) => {
const nodeNames = nodes.slice(0, i + 1).map(({ name }) => name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { promises as fs } from 'node:fs';
import { resolve } from 'node:path';
import type { DesignTokenGroup, DesignTokenSpecification } from '../design-token-specification.interface';
import type { DesignTokenVariableSet } from '../parsers';
import { computeFileToUpdatePath, getFileToUpdatePath, getTokenSorterByName, getTokenSorterByRef, renderDesignTokens } from './design-token-style.renderer';
import {
computeFileToUpdatePath,
getFileToUpdatePath, getTokenSorterByName,
getTokenSorterByRef,
getTokenSorterFromRegExpList,
renderDesignTokens
} from './design-token-style.renderer';

const rootPath = resolve('/');

Expand Down Expand Up @@ -180,6 +186,62 @@ describe('Design Token Renderer', () => {
});
});

describe('getTokenSorterFromRegExpList', () => {
it('should sort properly based on regExps', () => {
const regExps = [
/override$/,
/shadow/
];
const list = Array.from(designTokens.values());
const sortedTokens = getTokenSorterFromRegExpList(regExps)(designTokens)(list);

const listShadowIndex = list.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.test.shadow');
const listVar1Index = list.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.var1');
const sortedTokenVar1Index = sortedTokens.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.var1');
const sortedTokenShadowIndex = sortedTokens.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.test.shadow');

expect(list.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.var-expect-override'))
.toBeGreaterThan(listVar1Index);
expect(sortedTokens.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.var-expect-override'))
.toBeLessThan(sortedTokenVar1Index);

expect(listShadowIndex).toBeGreaterThan(listVar1Index);
expect(sortedTokenShadowIndex).toBeLessThan(sortedTokenVar1Index);

expect(listShadowIndex)
.toBeGreaterThan(list.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.example.var-expect-override'));
expect(sortedTokenShadowIndex)
.toBeGreaterThan(sortedTokens.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.example.var-expect-override'));
});

it('should not sort unmatched tokens', () => {
const regExps = [
/override$/,
/shadow/
];
const list = Array.from(designTokens.values());
const sortedTokens = getTokenSorterFromRegExpList(regExps)(designTokens)(list);

expect(sortedTokens.length).toBe(list.length);
expect(sortedTokens.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'last-group.last-token'))
.toBe(sortedTokens.length - 1);
});

it('should be correctly applied', () => {
const regExps = [
/-shadow/ // matching only the generated key (not the token name)
];

const list = Array.from(designTokens.values());
const sortedTokensBasedOnKeyPart = getTokenSorterFromRegExpList(regExps, false)(designTokens)(list);
const sortedTokensBasedOnRenderedKey = getTokenSorterFromRegExpList(regExps, true)(designTokens)(list);
const flattenListStr = list.map(({ tokenReferenceName }) => tokenReferenceName).join('');

expect(flattenListStr).toBe(sortedTokensBasedOnKeyPart.map(({ tokenReferenceName }) => tokenReferenceName).join(''));
expect(flattenListStr).not.toBe(sortedTokensBasedOnRenderedKey.map(({ tokenReferenceName }) => tokenReferenceName).join(''));
});
});

describe('getTokenSorterByName', () => {
let designTokensToSort!: DesignTokenVariableSet;
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getCssStyleContentUpdater } from './css/design-token-updater.renderers'
import type { Logger } from '@o3r/core';
import type { promises as fs } from 'node:fs';
import type { DesignTokenListTransform, DesignTokenRendererOptions } from './design-token.renderer.interface';
import { TOKEN_KEY_SEPARATOR } from '../parsers';

/**
* Retrieve the path of a target file based on root path if not absolute
Expand Down Expand Up @@ -144,6 +145,39 @@ export const getTokenSorterByRef: DesignTokenListTransform = (variableSet) => {
};
};

/**
* Reorganize the Tokens based on an ordered list of regexps.
* Each regexp is applied only to the last part of the Token name (before key rendering).
* @param regExps Ordered list of regular expressions defining the order of the Tokens.
* @param applyRendererName Determine if the regexps are applied to the rendered Token key. If `false`, it will be applied to the Token key's name (last part of the Token name).
*/
export const getTokenSorterFromRegExpList: (regExps: RegExp[], applyRendererName?: boolean) => DesignTokenListTransform = (regExps, applyRendererName = false) => (_variableSet, options) => {

const applyRegExp = (token: DesignTokenVariableStructure, regExp: RegExp) => (applyRendererName
? token.getKey(options?.tokenVariableNameRenderer)
: token.tokenReferenceName.split(TOKEN_KEY_SEPARATOR).at(-1)!
// eslint-disable-next-line unicorn/prefer-regexp-test -- to handle the global flag properly
).match(regExp);
kpanot marked this conversation as resolved.
Show resolved Hide resolved

return (tokens) =>
tokens
.map((token) => ({ index: regExps.findIndex((regExp) => applyRegExp(token, regExp)), token }))
.sort((a, b) => {
if (a.index === -1) {
if (b.index === -1) {
return 0;
}
return 1;
} else {
if (b.index === -1) {
return -1;
}
return b.index - a.index;
}
})
.map(({token}) => token);
};

/**
* Retrieve default file writer (based on Node `fs.promise.writeFile` interface)
* @param existsFile Function determining if the file exists
Expand Down
6 changes: 6 additions & 0 deletions packages/@o3r/design/testing/mocks/design-token-theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,11 @@
}
}
}
},
"last-group": {
"last-token": {
"$type": "color",
"$value": "#aaa"
}
}
}
Loading