mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-02 23:57:08 +08:00
731 lines
24 KiB
Python
731 lines
24 KiB
Python
"""
|
|
String manipulation nodes converted to ComfyUI v3 format.
|
|
|
|
This module contains v3 conversions of all string manipulation nodes from nodes_string.py.
|
|
The v3 implementations provide type safety, better documentation, and cleaner APIs
|
|
while maintaining full backward compatibility with v1 through the automatic
|
|
compatibility layer.
|
|
"""
|
|
|
|
import re
|
|
from comfy_api.v3 import io
|
|
|
|
|
|
class StringConcatenate(io.ComfyNodeV3):
|
|
"""Concatenates two strings with an optional delimiter between them."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="StringConcatenate",
|
|
display_name="String Concatenate",
|
|
category="utils/string",
|
|
description="Concatenates two strings together with an optional delimiter between them.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string_a",
|
|
display_name="String A",
|
|
multiline=True,
|
|
tooltip="The first string to concatenate",
|
|
),
|
|
io.String.Input(
|
|
"string_b",
|
|
display_name="String B",
|
|
multiline=True,
|
|
tooltip="The second string to concatenate",
|
|
),
|
|
io.String.Input(
|
|
"delimiter",
|
|
display_name="Delimiter",
|
|
default="",
|
|
multiline=False,
|
|
tooltip="The delimiter to insert between the two strings (empty by default)",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(
|
|
"concatenated",
|
|
display_name="Concatenated String",
|
|
tooltip="The result of concatenating string_a and string_b with the delimiter",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, string_a: str, string_b: str, delimiter: str) -> io.NodeOutput:
|
|
"""Concatenates two strings with an optional delimiter."""
|
|
result = delimiter.join((string_a, string_b))
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
class StringSubstring(io.ComfyNodeV3):
|
|
"""Extracts a substring from a string using start and end indices."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="StringSubstring",
|
|
display_name="String Substring",
|
|
category="utils/string",
|
|
description="Extracts a portion of a string using Python slice notation [start:end].",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to extract a substring from",
|
|
),
|
|
io.Int.Input(
|
|
"start",
|
|
display_name="Start Index",
|
|
tooltip="Starting position (inclusive). Negative values count from the end",
|
|
),
|
|
io.Int.Input(
|
|
"end",
|
|
display_name="End Index",
|
|
tooltip="Ending position (exclusive). Negative values count from the end",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(
|
|
"substring",
|
|
display_name="Substring",
|
|
tooltip="The extracted substring",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, string: str, start: int, end: int) -> io.NodeOutput:
|
|
"""Extracts substring using Python slice notation."""
|
|
return io.NodeOutput(string[start:end])
|
|
|
|
|
|
class StringLength(io.ComfyNodeV3):
|
|
"""Returns the length of a string."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="StringLength",
|
|
display_name="String Length",
|
|
category="utils/string",
|
|
description="Calculates the number of characters in a string.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to measure",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.Int.Output(
|
|
"length",
|
|
display_name="Length",
|
|
tooltip="The number of characters in the string",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, string: str) -> io.NodeOutput:
|
|
"""Returns the length of the input string."""
|
|
return io.NodeOutput(len(string))
|
|
|
|
|
|
class CaseConverter(io.ComfyNodeV3):
|
|
"""Converts string case to uppercase, lowercase, capitalize, or title case."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="CaseConverter",
|
|
display_name="Case Converter",
|
|
category="utils/string",
|
|
description="Converts text to different case formats.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to convert",
|
|
),
|
|
io.Combo.Input(
|
|
"mode",
|
|
display_name="Mode",
|
|
options=["UPPERCASE", "lowercase", "Capitalize", "Title Case"],
|
|
tooltip="The case conversion mode to apply",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(
|
|
"converted",
|
|
display_name="Converted String",
|
|
tooltip="The string with the selected case conversion applied",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, string: str, mode: str) -> io.NodeOutput:
|
|
"""Converts string to the selected case format."""
|
|
if mode == "UPPERCASE":
|
|
result = string.upper()
|
|
elif mode == "lowercase":
|
|
result = string.lower()
|
|
elif mode == "Capitalize":
|
|
result = string.capitalize()
|
|
elif mode == "Title Case":
|
|
result = string.title()
|
|
else:
|
|
result = string
|
|
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
class StringTrim(io.ComfyNodeV3):
|
|
"""Removes whitespace from the beginning, end, or both sides of a string."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="StringTrim",
|
|
display_name="String Trim",
|
|
category="utils/string",
|
|
description="Removes leading and/or trailing whitespace from a string.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to trim",
|
|
),
|
|
io.Combo.Input(
|
|
"mode",
|
|
display_name="Mode",
|
|
options=["Both", "Left", "Right"],
|
|
tooltip="Which side(s) to trim whitespace from",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(
|
|
"trimmed",
|
|
display_name="Trimmed String",
|
|
tooltip="The string with whitespace removed",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, string: str, mode: str) -> io.NodeOutput:
|
|
"""Removes whitespace based on the selected mode."""
|
|
if mode == "Both":
|
|
result = string.strip()
|
|
elif mode == "Left":
|
|
result = string.lstrip()
|
|
elif mode == "Right":
|
|
result = string.rstrip()
|
|
else:
|
|
result = string
|
|
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
class StringReplace(io.ComfyNodeV3):
|
|
"""Replaces all occurrences of a substring with another string."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="StringReplace",
|
|
display_name="String Replace",
|
|
category="utils/string",
|
|
description="Replaces all occurrences of a substring within a string.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to search in",
|
|
),
|
|
io.String.Input(
|
|
"find",
|
|
display_name="Find",
|
|
multiline=True,
|
|
tooltip="The substring to search for",
|
|
),
|
|
io.String.Input(
|
|
"replace",
|
|
display_name="Replace",
|
|
multiline=True,
|
|
tooltip="The string to replace matches with",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(
|
|
"result",
|
|
display_name="Result",
|
|
tooltip="The string with all replacements made",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, string: str, find: str, replace: str) -> io.NodeOutput:
|
|
"""Replaces all occurrences of find with replace."""
|
|
result = string.replace(find, replace)
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
class StringContains(io.ComfyNodeV3):
|
|
"""Checks if a string contains a substring."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="StringContains",
|
|
display_name="String Contains",
|
|
category="utils/string",
|
|
description="Checks whether a string contains a specific substring.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to search in",
|
|
),
|
|
io.String.Input(
|
|
"substring",
|
|
display_name="Substring",
|
|
multiline=True,
|
|
tooltip="The substring to search for",
|
|
),
|
|
io.Boolean.Input(
|
|
"case_sensitive",
|
|
display_name="Case Sensitive",
|
|
default=True,
|
|
tooltip="Whether the search should be case sensitive",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.Boolean.Output(
|
|
"contains",
|
|
display_name="Contains",
|
|
tooltip="True if the substring is found, False otherwise",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(
|
|
cls, string: str, substring: str, case_sensitive: bool
|
|
) -> io.NodeOutput:
|
|
"""Checks if string contains substring with optional case sensitivity."""
|
|
if case_sensitive:
|
|
contains = substring in string
|
|
else:
|
|
contains = substring.lower() in string.lower()
|
|
|
|
return io.NodeOutput(contains)
|
|
|
|
|
|
class StringCompare(io.ComfyNodeV3):
|
|
"""Compares two strings with various comparison modes."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="StringCompare",
|
|
display_name="String Compare",
|
|
category="utils/string",
|
|
description="Compares two strings using different comparison modes.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string_a",
|
|
display_name="String A",
|
|
multiline=True,
|
|
tooltip="The first string to compare",
|
|
),
|
|
io.String.Input(
|
|
"string_b",
|
|
display_name="String B",
|
|
multiline=True,
|
|
tooltip="The second string to compare",
|
|
),
|
|
io.Combo.Input(
|
|
"mode",
|
|
display_name="Mode",
|
|
options=["Starts With", "Ends With", "Equal"],
|
|
tooltip="The comparison mode to use",
|
|
),
|
|
io.Boolean.Input(
|
|
"case_sensitive",
|
|
display_name="Case Sensitive",
|
|
default=True,
|
|
tooltip="Whether the comparison should be case sensitive",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.Boolean.Output(
|
|
"result",
|
|
display_name="Result",
|
|
tooltip="True if the comparison succeeds, False otherwise",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(
|
|
cls, string_a: str, string_b: str, mode: str, case_sensitive: bool
|
|
) -> io.NodeOutput:
|
|
"""Compares two strings based on the selected mode and case sensitivity."""
|
|
if case_sensitive:
|
|
a = string_a
|
|
b = string_b
|
|
else:
|
|
a = string_a.lower()
|
|
b = string_b.lower()
|
|
|
|
if mode == "Equal":
|
|
result = a == b
|
|
elif mode == "Starts With":
|
|
result = a.startswith(b)
|
|
elif mode == "Ends With":
|
|
result = a.endswith(b)
|
|
else:
|
|
result = False
|
|
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
class RegexMatch(io.ComfyNodeV3):
|
|
"""Tests if a string matches a regular expression pattern."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="RegexMatch",
|
|
display_name="Regex Match",
|
|
category="utils/string",
|
|
description="Tests whether a string matches a regular expression pattern.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to test",
|
|
),
|
|
io.String.Input(
|
|
"regex_pattern",
|
|
display_name="Regex Pattern",
|
|
multiline=True,
|
|
tooltip="The regular expression pattern to match against",
|
|
),
|
|
io.Boolean.Input(
|
|
"case_insensitive",
|
|
display_name="Case Insensitive",
|
|
default=True,
|
|
tooltip="Whether to ignore case when matching",
|
|
),
|
|
io.Boolean.Input(
|
|
"multiline",
|
|
display_name="Multiline",
|
|
default=False,
|
|
tooltip="Whether ^ and $ match line boundaries",
|
|
),
|
|
io.Boolean.Input(
|
|
"dotall",
|
|
display_name="Dot All",
|
|
default=False,
|
|
tooltip="Whether . matches newline characters",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.Boolean.Output(
|
|
"matches",
|
|
display_name="Matches",
|
|
tooltip="True if the pattern matches, False otherwise",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(
|
|
cls,
|
|
string: str,
|
|
regex_pattern: str,
|
|
case_insensitive: bool,
|
|
multiline: bool,
|
|
dotall: bool,
|
|
) -> io.NodeOutput:
|
|
"""Tests if string matches the regex pattern."""
|
|
flags = 0
|
|
|
|
if case_insensitive:
|
|
flags |= re.IGNORECASE
|
|
if multiline:
|
|
flags |= re.MULTILINE
|
|
if dotall:
|
|
flags |= re.DOTALL
|
|
|
|
try:
|
|
match = re.search(regex_pattern, string, flags)
|
|
result = match is not None
|
|
except re.error:
|
|
result = False
|
|
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
class RegexExtract(io.ComfyNodeV3):
|
|
"""Extracts text from a string using regular expression patterns."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="RegexExtract",
|
|
display_name="Regex Extract",
|
|
category="utils/string",
|
|
description="Extracts text from a string using regular expression patterns and groups.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to extract from",
|
|
),
|
|
io.String.Input(
|
|
"regex_pattern",
|
|
display_name="Regex Pattern",
|
|
multiline=True,
|
|
tooltip="The regular expression pattern with optional groups",
|
|
),
|
|
io.Combo.Input(
|
|
"mode",
|
|
display_name="Mode",
|
|
options=["First Match", "All Matches", "First Group", "All Groups"],
|
|
tooltip="What to extract from the matches",
|
|
),
|
|
io.Boolean.Input(
|
|
"case_insensitive",
|
|
display_name="Case Insensitive",
|
|
default=True,
|
|
tooltip="Whether to ignore case when matching",
|
|
),
|
|
io.Boolean.Input(
|
|
"multiline",
|
|
display_name="Multiline",
|
|
default=False,
|
|
tooltip="Whether ^ and $ match line boundaries",
|
|
),
|
|
io.Boolean.Input(
|
|
"dotall",
|
|
display_name="Dot All",
|
|
default=False,
|
|
tooltip="Whether . matches newline characters",
|
|
),
|
|
io.Int.Input(
|
|
"group_index",
|
|
display_name="Group Index",
|
|
default=1,
|
|
min=0,
|
|
max=100,
|
|
tooltip="Which capture group to extract (0 = entire match)",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(
|
|
"extracted",
|
|
display_name="Extracted",
|
|
tooltip="The extracted text (multiple matches joined with newlines)",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(
|
|
cls,
|
|
string: str,
|
|
regex_pattern: str,
|
|
mode: str,
|
|
case_insensitive: bool,
|
|
multiline: bool,
|
|
dotall: bool,
|
|
group_index: int,
|
|
) -> io.NodeOutput:
|
|
"""Extracts text based on regex pattern and mode."""
|
|
join_delimiter = "\n"
|
|
|
|
flags = 0
|
|
if case_insensitive:
|
|
flags |= re.IGNORECASE
|
|
if multiline:
|
|
flags |= re.MULTILINE
|
|
if dotall:
|
|
flags |= re.DOTALL
|
|
|
|
try:
|
|
if mode == "First Match":
|
|
match = re.search(regex_pattern, string, flags)
|
|
if match:
|
|
result = match.group(0)
|
|
else:
|
|
result = ""
|
|
|
|
elif mode == "All Matches":
|
|
matches = re.findall(regex_pattern, string, flags)
|
|
if matches:
|
|
if isinstance(matches[0], tuple):
|
|
result = join_delimiter.join([m[0] for m in matches])
|
|
else:
|
|
result = join_delimiter.join(matches)
|
|
else:
|
|
result = ""
|
|
|
|
elif mode == "First Group":
|
|
match = re.search(regex_pattern, string, flags)
|
|
if match and len(match.groups()) >= group_index:
|
|
result = match.group(group_index)
|
|
else:
|
|
result = ""
|
|
|
|
elif mode == "All Groups":
|
|
matches = re.finditer(regex_pattern, string, flags)
|
|
results = []
|
|
for match in matches:
|
|
if match.groups() and len(match.groups()) >= group_index:
|
|
results.append(match.group(group_index))
|
|
result = join_delimiter.join(results)
|
|
else:
|
|
result = ""
|
|
|
|
except re.error:
|
|
result = ""
|
|
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
class RegexReplace(io.ComfyNodeV3):
|
|
"""Find and replace text using regex patterns."""
|
|
|
|
@classmethod
|
|
def DEFINE_SCHEMA(cls):
|
|
return io.SchemaV3(
|
|
node_id="RegexReplace",
|
|
display_name="Regex Replace",
|
|
category="utils/string",
|
|
description="Find and replace text using regular expression patterns.",
|
|
inputs=[
|
|
io.String.Input(
|
|
"string",
|
|
display_name="String",
|
|
multiline=True,
|
|
tooltip="The string to perform replacements on",
|
|
),
|
|
io.String.Input(
|
|
"regex_pattern",
|
|
display_name="Regex Pattern",
|
|
multiline=True,
|
|
tooltip="The regular expression pattern to match",
|
|
),
|
|
io.String.Input(
|
|
"replace",
|
|
display_name="Replace",
|
|
multiline=True,
|
|
tooltip="The replacement text (can use \\1, \\2 for capture groups)",
|
|
),
|
|
io.Boolean.Input(
|
|
"case_insensitive",
|
|
display_name="Case Insensitive",
|
|
default=True,
|
|
optional=True,
|
|
tooltip="Whether to ignore case when matching",
|
|
),
|
|
io.Boolean.Input(
|
|
"multiline",
|
|
display_name="Multiline",
|
|
default=False,
|
|
optional=True,
|
|
tooltip="Whether ^ and $ match line boundaries",
|
|
),
|
|
io.Boolean.Input(
|
|
"dotall",
|
|
display_name="Dot All",
|
|
default=False,
|
|
optional=True,
|
|
tooltip="When enabled, the dot (.) character will match any character including newline characters. When disabled, dots won't match newlines.",
|
|
),
|
|
io.Int.Input(
|
|
"count",
|
|
display_name="Count",
|
|
default=0,
|
|
min=0,
|
|
max=100,
|
|
optional=True,
|
|
tooltip="Maximum number of replacements to make. Set to 0 to replace all occurrences (default). Set to 1 to replace only the first match, 2 for the first two matches, etc.",
|
|
),
|
|
],
|
|
outputs=[
|
|
io.String.Output(
|
|
"result",
|
|
display_name="Result",
|
|
tooltip="The string with replacements made",
|
|
),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(
|
|
cls,
|
|
string: str,
|
|
regex_pattern: str,
|
|
replace: str,
|
|
case_insensitive: bool = True,
|
|
multiline: bool = False,
|
|
dotall: bool = False,
|
|
count: int = 0,
|
|
) -> io.NodeOutput:
|
|
"""Replaces text matching regex pattern."""
|
|
flags = 0
|
|
|
|
if case_insensitive:
|
|
flags |= re.IGNORECASE
|
|
if multiline:
|
|
flags |= re.MULTILINE
|
|
if dotall:
|
|
flags |= re.DOTALL
|
|
|
|
result = re.sub(regex_pattern, replace, string, count=count, flags=flags)
|
|
return io.NodeOutput(result)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"StringConcatenate": StringConcatenate,
|
|
"StringSubstring": StringSubstring,
|
|
"StringLength": StringLength,
|
|
"CaseConverter": CaseConverter,
|
|
"StringTrim": StringTrim,
|
|
"StringReplace": StringReplace,
|
|
"StringContains": StringContains,
|
|
"StringCompare": StringCompare,
|
|
"RegexMatch": RegexMatch,
|
|
"RegexExtract": RegexExtract,
|
|
"RegexReplace": RegexReplace,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"StringConcatenate": "Concatenate",
|
|
"StringSubstring": "Substring",
|
|
"StringLength": "Length",
|
|
"CaseConverter": "Case Converter",
|
|
"StringTrim": "Trim",
|
|
"StringReplace": "Replace",
|
|
"StringContains": "Contains",
|
|
"StringCompare": "Compare",
|
|
"RegexMatch": "Regex Match",
|
|
"RegexExtract": "Regex Extract",
|
|
"RegexReplace": "Regex Replace",
|
|
}
|
|
|