Skip to content

Commit

Permalink
add new string nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
mck committed Dec 12, 2024
1 parent 5729170 commit 6d9b5c3
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 63 deletions.
8 changes: 8 additions & 0 deletions .changeset/new-string-nodes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@tokens-studio/graph-engine": minor
---

Added new string manipulation nodes:
- Case Convert: Transform strings between camelCase, snake_case, kebab-case, and PascalCase
- Replace: Simple string replacement without regex
- Normalize: String normalization with accent removal options
62 changes: 0 additions & 62 deletions packages/documentation/docs/nodes/string/lowercase.mdx

This file was deleted.

2 changes: 1 addition & 1 deletion packages/graph-editor/src/data/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = '4.3.4';
export const version = '4.3.6';
86 changes: 86 additions & 0 deletions packages/graph-engine/src/nodes/string/case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { INodeDefinition, ToInput, ToOutput } from '../../index.js';
import { Node } from '../../programmatic/node.js';
import { StringSchema } from '../../schemas/index.js';

export enum CaseType {
CAMEL = 'camel',
SNAKE = 'snake',
KEBAB = 'kebab',
PASCAL = 'pascal'
}

/**
* This node converts strings between different case formats
*/
export default class NodeDefinition extends Node {
static title = 'Case Convert';
static type = 'studio.tokens.string.case';
static description = 'Converts strings between different case formats';

declare inputs: ToInput<{
string: string;
type: CaseType;
}>;
declare outputs: ToOutput<{
string: string;
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('string', {
type: StringSchema
});
this.addInput('type', {
type: {
...StringSchema,
enum: Object.values(CaseType),
default: CaseType.CAMEL
}
});
this.addOutput('string', {
type: StringSchema
});
}

execute(): void | Promise<void> {
const { string, type } = this.getAllInputs();

// First normalize the string by splitting on word boundaries
const words = string
// Add space before capitals in camelCase/PascalCase, but handle consecutive capitals
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
// Replace common delimiters with spaces
.replace(/[-_.]/g, ' ')
// Remove extra spaces and convert to lowercase
.trim()
.toLowerCase()
// Split into words and remove empty strings
.split(/\s+/)
.filter(word => word.length > 0);

let result: string;
switch (type) {
case CaseType.CAMEL:
result = words[0] + words.slice(1).map(capitalize).join('');
break;
case CaseType.SNAKE:
result = words.join('_');
break;
case CaseType.KEBAB:
result = words.join('-');
break;
case CaseType.PASCAL:
result = words.map(capitalize).join('');
break;
default:
result = string;
}

this.outputs.string.set(result);
}
}

function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
6 changes: 6 additions & 0 deletions packages/graph-engine/src/nodes/string/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import caseConvert from './case.js';
import interpolate from './interpolate.js';
import join from './join.js';
import lowercase from './lowercase.js';
import normalize from './normalize.js';
import pad from './pad.js';
import regex from './regex.js';
import replace from './replace.js';
import split from './split.js';
import stringify from './stringify.js';
import uppercase from './uppercase.js';

export const nodes = [
interpolate,
join,
caseConvert,
lowercase,
normalize,
pad,
regex,
replace,
split,
stringify,
uppercase
Expand Down
70 changes: 70 additions & 0 deletions packages/graph-engine/src/nodes/string/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { INodeDefinition, ToInput, ToOutput } from '../../index.js';
import { Node } from '../../programmatic/node.js';
import { StringSchema } from '../../schemas/index.js';

export enum NormalizationForm {
NFD = 'NFD',
NFC = 'NFC',
NFKD = 'NFKD',
NFKC = 'NFKC'
}

/**
* This node normalizes strings and can remove diacritical marks (accents)
*/
export default class NodeDefinition extends Node {
static title = 'Normalize';
static type = 'studio.tokens.string.normalize';
static description =
'Normalizes strings and optionally removes diacritical marks';

declare inputs: ToInput<{
string: string;
form: NormalizationForm;
removeAccents: boolean;
}>;
declare outputs: ToOutput<{
string: string;
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('string', {
type: StringSchema
});
this.addInput('form', {
type: {
...StringSchema,
enum: Object.values(NormalizationForm),
default: NormalizationForm.NFC
}
});
this.addInput('removeAccents', {
type: {
type: 'boolean',
title: 'Remove Accents',
description: 'Whether to remove diacritical marks',
default: true
}
});
this.addOutput('string', {
type: StringSchema
});
}

execute(): void | Promise<void> {
const { string, form, removeAccents } = this.getAllInputs();

let result = string.normalize(form);

if (removeAccents) {
// Remove combining diacritical marks
result = result
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.normalize(form);
}

this.outputs.string.set(result);
}
}
47 changes: 47 additions & 0 deletions packages/graph-engine/src/nodes/string/replace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { INodeDefinition, ToInput, ToOutput } from '../../index.js';
import { Node } from '../../programmatic/node.js';
import { StringSchema } from '../../schemas/index.js';

/**
* This node replaces all occurrences of a search string with a replacement string
*/
export default class NodeDefinition extends Node {
static title = 'Replace';
static type = 'studio.tokens.string.replace';
static description =
'Replaces all occurrences of a search string with a replacement string';

declare inputs: ToInput<{
string: string;
search: string;
replace: string;
}>;
declare outputs: ToOutput<{
string: string;
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('string', {
type: StringSchema
});
this.addInput('search', {
type: StringSchema
});
this.addInput('replace', {
type: {
...StringSchema,
default: ''
}
});
this.addOutput('string', {
type: StringSchema
});
}

execute(): void | Promise<void> {
const { string, search, replace } = this.getAllInputs();
const result = string.split(search).join(replace);
this.outputs.string.set(result);
}
}
65 changes: 65 additions & 0 deletions packages/graph-engine/tests/suites/nodes/string/case.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Graph } from '../../../../src/graph/graph.js';
import { describe, expect, test } from 'vitest';
import Node, { CaseType } from '../../../../src/nodes/string/case.js';

describe('string/case', () => {
test('should convert to camelCase', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.string.setValue('hello world test');
node.inputs.type.setValue(CaseType.CAMEL);

await node.execute();

expect(node.outputs.string.value).toBe('helloWorldTest');
});

test('should convert to snake_case', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.string.setValue('Hello World Test');
node.inputs.type.setValue(CaseType.SNAKE);

await node.execute();

expect(node.outputs.string.value).toBe('hello_world_test');
});

test('should convert to kebab-case', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.string.setValue('HelloWorld test');
node.inputs.type.setValue(CaseType.KEBAB);

await node.execute();

expect(node.outputs.string.value).toBe('hello-world-test');
});

test('should convert to PascalCase', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.string.setValue('hello_world_test');
node.inputs.type.setValue(CaseType.PASCAL);

await node.execute();

expect(node.outputs.string.value).toBe('HelloWorldTest');
});

test('should handle mixed input formats', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.string.setValue('some-mixed_FORMAT test');
node.inputs.type.setValue(CaseType.CAMEL);

await node.execute();

expect(node.outputs.string.value).toBe('someMixedFormatTest');
});
});
Loading

0 comments on commit 6d9b5c3

Please sign in to comment.