Skip to content

Add new string processing nodes#4010

Open
Keavon wants to merge 1 commit intomasterfrom
string-nodes
Open

Add new string processing nodes#4010
Keavon wants to merge 1 commit intomasterfrom
string-nodes

Conversation

@Keavon
Copy link
Copy Markdown
Member

@Keavon Keavon commented Apr 5, 2026

Still a bit more work to do to get this landed in the next day or so.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive set of string manipulation nodes to the node graph, including functionality for capitalization, joining, padding, repeating, and searching. It also updates the UI to support these nodes, specifically adding a property editor for string capitalization with joiner presets, and refines the string_slice node to use integer types. Review feedback pointed out a bug in the unescaping logic for string separators where the order of replacements could lead to incorrect results, and suggested improvements to the capitalization behavior when word-splitting is disabled to ensure consistency and standard formatting.

Comment on lines +337 to +341
let separator = if separator_escaping {
separator.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\0", "\0").replace("\\\\", "\\")
} else {
separator
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The chained replace() calls for unescaping the separator string are buggy. The order of replacements matters, and the current order can lead to incorrect results. For example, an input of "\\\\n" (meant to be a literal backslash followed by 'n') will be incorrectly converted to a newline character.

A more robust and efficient approach is to iterate through the string once and handle escape sequences manually. This avoids ordering issues and correctly handles all cases, including unrecognized escape sequences.

	let separator = if separator_escaping {
		let mut unescaped = String::with_capacity(separator.len());
		let mut chars = separator.chars();
		while let Some(c) = chars.next() {
			if c == '\\' {
				match chars.next() {
					Some('n') => unescaped.push('\n'),
					Some('r') => unescaped.push('\r'),
					Some('t') => unescaped.push('\t'),
					Some('0') => unescaped.push('\0'),
					Some('\\') => unescaped.push('\\'),
					Some(other) => {
						unescaped.push('\\');
						unescaped.push(other);
					}
					None => unescaped.push('\\'),
				}
			} else {
				unescaped.push(c);
			}
		}
		unescaped
	} else {
		separator
	};

Comment on lines +224 to +240
StringCapitalization::CapitalCase | StringCapitalization::HeadlineCase => {
let mut capitalize_next = true;
string
.chars()
.map(|c| {
if c.is_whitespace() || c == '_' || c == '-' {
capitalize_next = true;
c
} else if capitalize_next {
capitalize_next = false;
c.to_uppercase().next().unwrap_or(c)
} else {
c
}
})
.collect()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation for CapitalCase and HeadlineCase when use_joiner is disabled is inconsistent with the documentation and the behavior when use_joiner is enabled. It only uppercases the first letter of each word but doesn't lowercase the rest. This can lead to unexpected results (e.g., hELLO becomes HELLO instead of Hello).

To make the behavior consistent, you should also lowercase the other letters in each word. This would also make HeadlineCase a reasonable alias for CapitalCase in this branch, as implementing true headline casing without the titlecase crate is complex.

Suggested change
StringCapitalization::CapitalCase | StringCapitalization::HeadlineCase => {
let mut capitalize_next = true;
string
.chars()
.map(|c| {
if c.is_whitespace() || c == '_' || c == '-' {
capitalize_next = true;
c
} else if capitalize_next {
capitalize_next = false;
c.to_uppercase().next().unwrap_or(c)
} else {
c
}
})
.collect()
}
StringCapitalization::CapitalCase | StringCapitalization::HeadlineCase => {
let mut capitalize_next = true;
string
.chars()
.map(|c| {
if c.is_whitespace() || c == '_' || c == '-' {
capitalize_next = true;
c
} else if capitalize_next {
capitalize_next = false;
c.to_uppercase().next().unwrap_or(c)
} else {
c.to_lowercase().next().unwrap_or(c)
}
})
.collect()
}

Comment on lines +248 to +264
StringCapitalization::CamelCase => {
let mut capitalize_next = false;
string
.chars()
.map(|c| {
if c.is_whitespace() || c == '_' || c == '-' {
capitalize_next = true;
c
} else if capitalize_next {
capitalize_next = false;
c.to_uppercase().next().unwrap_or(c)
} else {
c.to_lowercase().next().unwrap_or(c)
}
})
.collect()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The !use_joiner implementation for CamelCase is non-standard and may be confusing. It lowercases the first word and then capitalizes the first letter of subsequent words (e.g., HELLO WORLD becomes hello World), which is not typical camelCase.

Given that !use_joiner implies preserving separators, a more predictable behavior might be to simply lowercase the first letter of the entire string, which is a common variation of camel case.

            StringCapitalization::CamelCase => {
				let mut chars = string.chars();
				match chars.next() {
					Some(first) => first.to_lowercase().to_string() + chars.as_str(),
					None => String::new(),
				}
			}

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 8 files

Confidence score: 3/5

  • There is concrete user-impact risk in node-graph/nodes/gcore/src/logic.rs: Unicode case conversion keeps only the first character from to_uppercase()/to_lowercase(), which can produce incorrect text for some characters/locales.
  • editor/src/messages/portfolio/document/node_graph/node_properties.rs has a UI logic inconsistency where UseJoiner can be hidden when JoinerInput is exposed, and node-graph/nodes/gcore/src/logic.rs also has a use_joiner behavior mismatch for LowerCase/UpperCase; together these are moderate regressions but likely localized.
  • The PR title format issue in node-graph/nodes/gcore/Cargo.toml is process-related rather than runtime functionality, so it should be fixed but does not by itself indicate breakage.
  • Pay close attention to node-graph/nodes/gcore/src/logic.rs and editor/src/messages/portfolio/document/node_graph/node_properties.rs - text transformation correctness and joiner control visibility/behavior need validation before merge.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="node-graph/nodes/gcore/Cargo.toml">

<violation number="1" location="node-graph/nodes/gcore/Cargo.toml:31">
P1: Custom agent: **PR title enforcement**

PR title does not follow the required "New nodes" dedicated format. Rename it to `New nodes: 'Name1', 'Name2', and 'Name3'` (with actual node names in single quotes).</violation>
</file>

<file name="editor/src/messages/portfolio/document/node_graph/node_properties.rs">

<violation number="1" location="editor/src/messages/portfolio/document/node_graph/node_properties.rs:1628">
P2: `UseJoiner` control is conditionally hidden when `JoinerInput` is exposed, because both controls are gated on `joiner_value` being non-exposed.</violation>
</file>

<file name="node-graph/nodes/gcore/src/logic.rs">

<violation number="1" location="node-graph/nodes/gcore/src/logic.rs:222">
P2: `use_joiner` is documented as applying to capitalization conversion, but `LowerCase`/`UpperCase` ignore `joiner` when it is enabled.

(Based on your team's feedback about aligning behavior with comments and TODO expectations.) [FEEDBACK_USED]</violation>

<violation number="2" location="node-graph/nodes/gcore/src/logic.rs:234">
P2: Unicode case conversion is lossy here because only the first character of `to_uppercase()`/`to_lowercase()` is kept.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -28,6 +28,9 @@ dyn-any = { workspace = true }
glam = { workspace = true }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: PR title enforcement

PR title does not follow the required "New nodes" dedicated format. Rename it to New nodes: 'Name1', 'Name2', and 'Name3' (with actual node names in single quotes).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/gcore/Cargo.toml, line 31:

<comment>PR title does not follow the required "New nodes" dedicated format. Rename it to `New nodes: 'Name1', 'Name2', and 'Name3'` (with actual node names in single quotes).</comment>

<file context>
@@ -28,6 +28,9 @@ dyn-any = { workspace = true }
 glam = { workspace = true }
 log = { workspace = true }
 serde_json = { workspace = true }
+convert_case = { workspace = true }
+titlecase = { workspace = true }
+unicode-segmentation = { workspace = true }
</file context>


// Joiner row: the UseJoiner checkbox is drawn in the assist area, followed by the Joiner text input
let mut joiner_widgets = start_widgets(ParameterWidgetsInfo::new(node_id, JoinerInput::INDEX, false, context));
if let Some(joiner) = joiner_value {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: UseJoiner control is conditionally hidden when JoinerInput is exposed, because both controls are gated on joiner_value being non-exposed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/node_graph/node_properties.rs, line 1628:

<comment>`UseJoiner` control is conditionally hidden when `JoinerInput` is exposed, because both controls are gated on `joiner_value` being non-exposed.</comment>

<file context>
@@ -1588,6 +1590,101 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC
+
+	// Joiner row: the UseJoiner checkbox is drawn in the assist area, followed by the Joiner text input
+	let mut joiner_widgets = start_widgets(ParameterWidgetsInfo::new(node_id, JoinerInput::INDEX, false, context));
+	if let Some(joiner) = joiner_value {
+		let joiner_is_empty = joiner.is_empty();
+		joiner_widgets.extend_from_slice(&[
</file context>

// When the joiner is disabled, apply only character-level casing while preserving the string's existing structure
if !use_joiner {
return match capitalization {
StringCapitalization::LowerCase => string.to_lowercase(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: use_joiner is documented as applying to capitalization conversion, but LowerCase/UpperCase ignore joiner when it is enabled.

(Based on your team's feedback about aligning behavior with comments and TODO expectations.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/gcore/src/logic.rs, line 222:

<comment>`use_joiner` is documented as applying to capitalization conversion, but `LowerCase`/`UpperCase` ignore `joiner` when it is enabled.

(Based on your team's feedback about aligning behavior with comments and TODO expectations.) </comment>

<file context>
@@ -59,6 +85,208 @@ fn string_slice(_: impl Ctx, string: String, start: f64, end: f64) -> String {
+	// When the joiner is disabled, apply only character-level casing while preserving the string's existing structure
+	if !use_joiner {
+		return match capitalization {
+			StringCapitalization::LowerCase => string.to_lowercase(),
+			StringCapitalization::UpperCase => string.to_uppercase(),
+			StringCapitalization::CapitalCase | StringCapitalization::HeadlineCase => {
</file context>

c
} else if capitalize_next {
capitalize_next = false;
c.to_uppercase().next().unwrap_or(c)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Unicode case conversion is lossy here because only the first character of to_uppercase()/to_lowercase() is kept.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/gcore/src/logic.rs, line 234:

<comment>Unicode case conversion is lossy here because only the first character of `to_uppercase()`/`to_lowercase()` is kept.</comment>

<file context>
@@ -59,6 +85,208 @@ fn string_slice(_: impl Ctx, string: String, start: f64, end: f64) -> String {
+							c
+						} else if capitalize_next {
+							capitalize_next = false;
+							c.to_uppercase().next().unwrap_or(c)
+						} else {
+							c
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant