diff --git a/.changeset/new-string-nodes.md b/.changeset/new-string-nodes.md
new file mode 100644
index 000000000..b60222cef
--- /dev/null
+++ b/.changeset/new-string-nodes.md
@@ -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
\ No newline at end of file
diff --git a/packages/documentation/docs/nodes/string/lowercase.mdx b/packages/documentation/docs/nodes/string/lowercase.mdx
deleted file mode 100644
index 4de647693..000000000
--- a/packages/documentation/docs/nodes/string/lowercase.mdx
+++ /dev/null
@@ -1,62 +0,0 @@
----
-title: Lowercase
----
-import { Editor, PanelGroup, DropPanelStore, PanelItem } from '@tokens-studio/graph-editor';
-import {InitialLayout} from '@site/src/components/editor/layout';
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-
-# lowercase
-
-Node Type ID: `"studio.tokens.string.lowercase"`
-
-## Package
-
-This is a core package available at [Graph Engine](https://www.npmjs.com/package/@tokens-studio/graph-engine)
-
-To install
-
-
-
-```bash
-npm install @tokens-studio/graph-engine
-```
-
-
-```bash
-yarn add @tokens-studio/graph-engine
-```
-
-
-
-
-
-## Description
-Converts a string to lowercase
-
-## Inputs
-
-
-#### value
-
-*No description*
-
-
-
-
-## Outputs
-
-
-#### value
-
-*No description*
-
-
-
-
-
-## Example
-
-*No examples*
-
diff --git a/packages/graph-editor/src/data/version.ts b/packages/graph-editor/src/data/version.ts
index 0dea284ee..23911495f 100644
--- a/packages/graph-editor/src/data/version.ts
+++ b/packages/graph-editor/src/data/version.ts
@@ -1 +1 @@
-export const version = '4.3.4';
+export const version = '4.3.6';
diff --git a/packages/graph-engine/src/nodes/string/case.ts b/packages/graph-engine/src/nodes/string/case.ts
new file mode 100644
index 000000000..bfa6b5b68
--- /dev/null
+++ b/packages/graph-engine/src/nodes/string/case.ts
@@ -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 {
+ 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);
+}
diff --git a/packages/graph-engine/src/nodes/string/index.ts b/packages/graph-engine/src/nodes/string/index.ts
index 86910ed47..da8396e3f 100644
--- a/packages/graph-engine/src/nodes/string/index.ts
+++ b/packages/graph-engine/src/nodes/string/index.ts
@@ -1,8 +1,11 @@
+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';
@@ -10,9 +13,12 @@ import uppercase from './uppercase.js';
export const nodes = [
interpolate,
join,
+ caseConvert,
lowercase,
+ normalize,
pad,
regex,
+ replace,
split,
stringify,
uppercase
diff --git a/packages/graph-engine/src/nodes/string/normalize.ts b/packages/graph-engine/src/nodes/string/normalize.ts
new file mode 100644
index 000000000..8c0b2fbef
--- /dev/null
+++ b/packages/graph-engine/src/nodes/string/normalize.ts
@@ -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 {
+ 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);
+ }
+}
diff --git a/packages/graph-engine/src/nodes/string/replace.ts b/packages/graph-engine/src/nodes/string/replace.ts
new file mode 100644
index 000000000..84becbe23
--- /dev/null
+++ b/packages/graph-engine/src/nodes/string/replace.ts
@@ -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 {
+ const { string, search, replace } = this.getAllInputs();
+ const result = string.split(search).join(replace);
+ this.outputs.string.set(result);
+ }
+}
diff --git a/packages/graph-engine/tests/suites/nodes/string/case.test.ts b/packages/graph-engine/tests/suites/nodes/string/case.test.ts
new file mode 100644
index 000000000..c2ad32980
--- /dev/null
+++ b/packages/graph-engine/tests/suites/nodes/string/case.test.ts
@@ -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');
+ });
+});
diff --git a/packages/graph-engine/tests/suites/nodes/string/normalize.test.ts b/packages/graph-engine/tests/suites/nodes/string/normalize.test.ts
new file mode 100644
index 000000000..2f80f9ed3
--- /dev/null
+++ b/packages/graph-engine/tests/suites/nodes/string/normalize.test.ts
@@ -0,0 +1,68 @@
+import { Graph } from '../../../../src/graph/graph.js';
+import { describe, expect, test } from 'vitest';
+import Node, {
+ NormalizationForm
+} from '../../../../src/nodes/string/normalize.js';
+
+describe('string/normalize', () => {
+ test('should remove accents when removeAccents is true', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ node.inputs.string.setValue('café résumé');
+ node.inputs.removeAccents.setValue(true);
+ node.inputs.form.setValue(NormalizationForm.NFC);
+
+ await node.execute();
+
+ expect(node.outputs.string.value).toBe('cafe resume');
+ });
+
+ test('should preserve accents when removeAccents is false', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ node.inputs.string.setValue('café résumé');
+ node.inputs.removeAccents.setValue(false);
+ node.inputs.form.setValue(NormalizationForm.NFC);
+
+ await node.execute();
+
+ expect(node.outputs.string.value).toBe('café résumé');
+ });
+
+ test('should handle different normalization forms', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ // Using a string with a precomposed character (é) vs decomposed (e + ´)
+ const precomposed = 'café'; // é is a single character
+ const decomposed = 'cafe\u0301'; // e and ´ are separate characters
+
+ node.inputs.string.setValue(decomposed);
+ node.inputs.removeAccents.setValue(false);
+
+ // Test NFC (precomposed)
+ node.inputs.form.setValue(NormalizationForm.NFC);
+ await node.execute();
+ expect(node.outputs.string.value).toBe(precomposed);
+
+ // Test NFD (decomposed)
+ node.inputs.form.setValue(NormalizationForm.NFD);
+ await node.execute();
+ expect(node.outputs.string.value).toBe(decomposed);
+ });
+
+ test('should handle empty string', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ node.inputs.string.setValue('');
+ node.inputs.removeAccents.setValue(true);
+ node.inputs.form.setValue(NormalizationForm.NFC);
+
+ await node.execute();
+
+ expect(node.outputs.string.value).toBe('');
+ });
+});
diff --git a/packages/graph-engine/tests/suites/nodes/string/replace.test.ts b/packages/graph-engine/tests/suites/nodes/string/replace.test.ts
new file mode 100644
index 000000000..1916e055d
--- /dev/null
+++ b/packages/graph-engine/tests/suites/nodes/string/replace.test.ts
@@ -0,0 +1,57 @@
+import { Graph } from '../../../../src/graph/graph.js';
+import { describe, expect, test } from 'vitest';
+import Node from '../../../../src/nodes/string/replace.js';
+
+describe('string/replace', () => {
+ test('should replace all occurrences of a string', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ node.inputs.string.setValue('hello hello world');
+ node.inputs.search.setValue('hello');
+ node.inputs.replace.setValue('hi');
+
+ await node.execute();
+
+ expect(node.outputs.string.value).toBe('hi hi world');
+ });
+
+ test('should handle empty replacement string', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ node.inputs.string.setValue('hello world');
+ node.inputs.search.setValue('hello ');
+ node.inputs.replace.setValue('');
+
+ await node.execute();
+
+ expect(node.outputs.string.value).toBe('world');
+ });
+
+ test('should handle search string not found', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ node.inputs.string.setValue('hello world');
+ node.inputs.search.setValue('xyz');
+ node.inputs.replace.setValue('abc');
+
+ await node.execute();
+
+ expect(node.outputs.string.value).toBe('hello world');
+ });
+
+ test('should handle empty input string', async () => {
+ const graph = new Graph();
+ const node = new Node({ graph });
+
+ node.inputs.string.setValue('');
+ node.inputs.search.setValue('test');
+ node.inputs.replace.setValue('replace');
+
+ await node.execute();
+
+ expect(node.outputs.string.value).toBe('');
+ });
+});