Skip to content
Draft
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 @@ -908,6 +908,45 @@ describe('editToken', () => {
]);
});

it('should preserve ordering of numeric keys when saving JSON data', () => {
store.dispatch.tokenState.setJSONData(`{
"color": {
"base_palette": {
"0950": {
"value": "#095000",
"type": "color"
},
"1000": {
"value": "#100000",
"type": "color"
}
}
}
}`);
const { tokens } = store.getState().tokenState;
expect(tokens.global[0].name).toEqual('color.base_palette.0950');
expect(tokens.global[1].name).toEqual('color.base_palette.1000');

// Now reverse the order in the JSON string
store.dispatch.tokenState.setJSONData(`{
"color": {
"base_palette": {
"1000": {
"value": "#100000",
"type": "color"
},
"0950": {
"value": "#095000",
"type": "color"
}
}
}
}`);
const { tokens: updatedTokens } = store.getState().tokenState;
expect(updatedTokens.global[0].name).toEqual('color.base_palette.1000');
expect(updatedTokens.global[1].name).toEqual('color.base_palette.0950');
});

it('can duplicate token', () => {
store.dispatch.tokenState.duplicateToken({
newName: 'primary-copy',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as tokenStateEffects from './effects/tokenState';
import parseTokenValues from '@/utils/parseTokenValues';
import { notifyToUI } from '@/plugin/notifiers';
import parseJson from '@/utils/parseJson';
import { extractJsonKeysOrder } from '@/utils/extractJsonKeysOrder';
import { TokenData } from '@/types/SecondScreen';
import updateTokensOnSources from '../updateSources';
import {
Expand Down Expand Up @@ -58,8 +59,6 @@ import { singleTokensToRawTokenSet } from '@/utils/convert';
import { checkStorageSize } from '@/utils/checkStorageSize';
import { compareLastSyncedState } from '@/utils/compareLastSyncedState';



/** Context required to call the Studio gRPC-backed resolver endpoint */
export interface ServerResolverContext {
projectId: string;
Expand Down Expand Up @@ -270,6 +269,26 @@ export const tokenState = createModel<RootModel>()({
const parsedTokens = parseJson(payload);
parseTokenValues(parsedTokens, true);
const values = parseTokenValues({ [state.activeTokenSet]: parsedTokens }, true);
const activeSetTokens = values[state.activeTokenSet];
if (activeSetTokens && Array.isArray(activeSetTokens)) {
const orderedNames = extractJsonKeysOrder(payload);
const nameToIndex = new Map<string, number>();
orderedNames.forEach((name, index) => {
if (!nameToIndex.has(name)) {
nameToIndex.set(name, index);
}
});
activeSetTokens.sort((a, b) => {
const indexA = nameToIndex.get(a.name);
const indexB = nameToIndex.get(b.name);
if (indexA !== undefined && indexB !== undefined) {
return indexA - indexB;
}
if (indexA !== undefined) return -1;
if (indexB !== undefined) return 1;
return 0;
});
}
return {
...state,
tokens: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { extractJsonKeysOrder } from '../extractJsonKeysOrder';

describe('extractJsonKeysOrder', () => {
it('should extract keys in order of appearance', () => {
const json = `{
"color": {
"base_palette": {
"0000": { "value": "#000000" },
"1000": { "value": "#ffffff" }
}
}
}`;
expect(extractJsonKeysOrder(json)).toEqual([
'color',
'color.base_palette',
'color.base_palette.0000',
'color.base_palette.0000.value',
'color.base_palette.1000',
'color.base_palette.1000.value',
]);
});

it('should skip comments and whitespaces', () => {
const json = `{
// a comment here
"color"/* another comment */: {
"base_palette": {
"0000": { "value": "#000000" },
"1000": { "value": "#ffffff" }
}
}
}`;
expect(extractJsonKeysOrder(json)).toEqual([
'color',
'color.base_palette',
'color.base_palette.0000',
'color.base_palette.0000.value',
'color.base_palette.1000',
'color.base_palette.1000.value',
]);
});

it('should handle arrays correctly without messing up the stack', () => {
const json = `{
"shadows": [
{
"type": "innerShadow",
"color": "#000000"
}
],
"otherKey": "value"
}`;
expect(extractJsonKeysOrder(json)).toEqual([
'shadows',
'type',
'color',
'otherKey',
]);
});

it('should ignore braces and colons inside string values', () => {
const json = `{
"token": {
"value": "{colors.base_palette.0000}",
"type": "color"
}
}`;
expect(extractJsonKeysOrder(json)).toEqual([
'token',
'token.value',
'token.type',
]);
});
});
95 changes: 95 additions & 0 deletions packages/tokens-studio-for-figma/src/utils/extractJsonKeysOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export function extractJsonKeysOrder(json: string): string[] {
const paths: string[] = [];
const stack: string[] = [];
let i = 0;
let arrayDepth = 0;

const skipWhitespaceAndComments = () => {
while (i < json.length) {
const char = json[i];
if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
i += 1;
} else if (char === '/' && json[i + 1] === '/') {
// Line comment
i += 2;
while (i < json.length && json[i] !== '\n' && json[i] !== '\r') {
i += 1;
}
} else if (char === '/' && json[i + 1] === '*') {
// Block comment
i += 2;
while (i < json.length && !(json[i] === '*' && json[i + 1] === '/')) {
i += 1;
}
if (i < json.length) i += 2;
} else {
break;
}
}
};

const parseString = (): string | null => {
const quote = json[i];
if (quote !== '"' && quote !== "'") return null;
i += 1; // consume quote
let str = '';
while (i < json.length) {
const char = json[i];
if (char === quote) {
i += 1; // consume quote
return str;
}
if (char === '\\') {
str += json[i + 1] || '';
i += 2;
} else {
str += char;
i += 1;
}
}
return str;
};

while (i < json.length) {
skipWhitespaceAndComments();
if (i >= json.length) break;

const char = json[i];
if (char === '{') {
i += 1;
} else if (char === '}') {
if (arrayDepth === 0) {
stack.pop();
}
i += 1;
} else if (char === '[') {
arrayDepth += 1;
i += 1;
} else if (char === ']') {
arrayDepth -= 1;
i += 1;
} else if (char === '"' || char === "'") {
const str = parseString();
if (str !== null) {
skipWhitespaceAndComments();
if (json[i] === ':') {
i += 1; // consume ':'
skipWhitespaceAndComments();
const fullPath = [...stack, str].join('.');
paths.push(fullPath);

// Check if this key opens a nested object
if (json[i] === '{' && arrayDepth === 0) {
stack.push(str);
}
}
} else {
i += 1;
}
} else {
i += 1;
}
}

return paths;
}
Loading