convert String nodes to V3 schema (#9370)

This commit is contained in:
Alexander Piskun
2025-08-22 05:03:57 +03:00
committed by GitHub
parent 1b2de2642d
commit bc49106837

View File

@@ -1,77 +1,91 @@
import re import re
from typing_extensions import override
from comfy.comfy_types.node_typing import IO from comfy_api.latest import ComfyExtension, io
class StringConcatenate():
class StringConcatenate(io.ComfyNode):
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="StringConcatenate",
"string_a": (IO.STRING, {"multiline": True}), display_name="Concatenate",
"string_b": (IO.STRING, {"multiline": True}), category="utils/string",
"delimiter": (IO.STRING, {"multiline": False, "default": ""}) inputs=[
} io.String.Input("string_a", multiline=True),
} io.String.Input("string_b", multiline=True),
io.String.Input("delimiter", multiline=False, default=""),
],
outputs=[
io.String.Output(),
]
)
RETURN_TYPES = (IO.STRING,)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string_a, string_b, delimiter, **kwargs):
return delimiter.join((string_a, string_b)),
class StringSubstring():
@classmethod @classmethod
def INPUT_TYPES(s): def execute(cls, string_a, string_b, delimiter):
return { return io.NodeOutput(delimiter.join((string_a, string_b)))
"required": {
"string": (IO.STRING, {"multiline": True}),
"start": (IO.INT, {}),
"end": (IO.INT, {}),
}
}
RETURN_TYPES = (IO.STRING,)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string, start, end, **kwargs): class StringSubstring(io.ComfyNode):
return string[start:end],
class StringLength():
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="StringSubstring",
"string": (IO.STRING, {"multiline": True}) display_name="Substring",
} category="utils/string",
} inputs=[
io.String.Input("string", multiline=True),
io.Int.Input("start"),
io.Int.Input("end"),
],
outputs=[
io.String.Output(),
]
)
RETURN_TYPES = (IO.INT,)
RETURN_NAMES = ("length",)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string, **kwargs):
length = len(string)
return length,
class CaseConverter():
@classmethod @classmethod
def INPUT_TYPES(s): def execute(cls, string, start, end):
return { return io.NodeOutput(string[start:end])
"required": {
"string": (IO.STRING, {"multiline": True}),
"mode": (IO.COMBO, {"options": ["UPPERCASE", "lowercase", "Capitalize", "Title Case"]})
}
}
RETURN_TYPES = (IO.STRING,)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string, mode, **kwargs): class StringLength(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringLength",
display_name="Length",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
],
outputs=[
io.Int.Output(display_name="length"),
]
)
@classmethod
def execute(cls, string):
return io.NodeOutput(len(string))
class CaseConverter(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CaseConverter",
display_name="Case Converter",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["UPPERCASE", "lowercase", "Capitalize", "Title Case"]),
],
outputs=[
io.String.Output(),
]
)
@classmethod
def execute(cls, string, mode):
if mode == "UPPERCASE": if mode == "UPPERCASE":
result = string.upper() result = string.upper()
elif mode == "lowercase": elif mode == "lowercase":
@@ -83,24 +97,27 @@ class CaseConverter():
else: else:
result = string result = string
return result, return io.NodeOutput(result)
class StringTrim(): class StringTrim(io.ComfyNode):
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="StringTrim",
"string": (IO.STRING, {"multiline": True}), display_name="Trim",
"mode": (IO.COMBO, {"options": ["Both", "Left", "Right"]}) category="utils/string",
} inputs=[
} io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["Both", "Left", "Right"]),
],
outputs=[
io.String.Output(),
]
)
RETURN_TYPES = (IO.STRING,) @classmethod
FUNCTION = "execute" def execute(cls, string, mode):
CATEGORY = "utils/string"
def execute(self, string, mode, **kwargs):
if mode == "Both": if mode == "Both":
result = string.strip() result = string.strip()
elif mode == "Left": elif mode == "Left":
@@ -110,70 +127,78 @@ class StringTrim():
else: else:
result = string result = string
return result, return io.NodeOutput(result)
class StringReplace():
class StringReplace(io.ComfyNode):
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="StringReplace",
"string": (IO.STRING, {"multiline": True}), display_name="Replace",
"find": (IO.STRING, {"multiline": True}), category="utils/string",
"replace": (IO.STRING, {"multiline": True}) inputs=[
} io.String.Input("string", multiline=True),
} io.String.Input("find", multiline=True),
io.String.Input("replace", multiline=True),
],
outputs=[
io.String.Output(),
]
)
RETURN_TYPES = (IO.STRING,)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string, find, replace, **kwargs):
result = string.replace(find, replace)
return result,
class StringContains():
@classmethod @classmethod
def INPUT_TYPES(s): def execute(cls, string, find, replace):
return { return io.NodeOutput(string.replace(find, replace))
"required": {
"string": (IO.STRING, {"multiline": True}),
"substring": (IO.STRING, {"multiline": True}),
"case_sensitive": (IO.BOOLEAN, {"default": True})
}
}
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("contains",)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string, substring, case_sensitive, **kwargs): class StringContains(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringContains",
display_name="Contains",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("substring", multiline=True),
io.Boolean.Input("case_sensitive", default=True),
],
outputs=[
io.Boolean.Output(display_name="contains"),
]
)
@classmethod
def execute(cls, string, substring, case_sensitive):
if case_sensitive: if case_sensitive:
contains = substring in string contains = substring in string
else: else:
contains = substring.lower() in string.lower() contains = substring.lower() in string.lower()
return contains, return io.NodeOutput(contains)
class StringCompare(): class StringCompare(io.ComfyNode):
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="StringCompare",
"string_a": (IO.STRING, {"multiline": True}), display_name="Compare",
"string_b": (IO.STRING, {"multiline": True}), category="utils/string",
"mode": (IO.COMBO, {"options": ["Starts With", "Ends With", "Equal"]}), inputs=[
"case_sensitive": (IO.BOOLEAN, {"default": True}) io.String.Input("string_a", multiline=True),
} io.String.Input("string_b", multiline=True),
} io.Combo.Input("mode", options=["Starts With", "Ends With", "Equal"]),
io.Boolean.Input("case_sensitive", default=True),
],
outputs=[
io.Boolean.Output(),
]
)
RETURN_TYPES = (IO.BOOLEAN,) @classmethod
FUNCTION = "execute" def execute(cls, string_a, string_b, mode, case_sensitive):
CATEGORY = "utils/string"
def execute(self, string_a, string_b, mode, case_sensitive, **kwargs):
if case_sensitive: if case_sensitive:
a = string_a a = string_a
b = string_b b = string_b
@@ -182,31 +207,34 @@ class StringCompare():
b = string_b.lower() b = string_b.lower()
if mode == "Equal": if mode == "Equal":
return a == b, return io.NodeOutput(a == b)
elif mode == "Starts With": elif mode == "Starts With":
return a.startswith(b), return io.NodeOutput(a.startswith(b))
elif mode == "Ends With": elif mode == "Ends With":
return a.endswith(b), return io.NodeOutput(a.endswith(b))
class RegexMatch():
class RegexMatch(io.ComfyNode):
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="RegexMatch",
"string": (IO.STRING, {"multiline": True}), display_name="Regex Match",
"regex_pattern": (IO.STRING, {"multiline": True}), category="utils/string",
"case_insensitive": (IO.BOOLEAN, {"default": True}), inputs=[
"multiline": (IO.BOOLEAN, {"default": False}), io.String.Input("string", multiline=True),
"dotall": (IO.BOOLEAN, {"default": False}) io.String.Input("regex_pattern", multiline=True),
} io.Boolean.Input("case_insensitive", default=True),
} io.Boolean.Input("multiline", default=False),
io.Boolean.Input("dotall", default=False),
],
outputs=[
io.Boolean.Output(display_name="matches"),
]
)
RETURN_TYPES = (IO.BOOLEAN,) @classmethod
RETURN_NAMES = ("matches",) def execute(cls, string, regex_pattern, case_insensitive, multiline, dotall):
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string, regex_pattern, case_insensitive, multiline, dotall, **kwargs):
flags = 0 flags = 0
if case_insensitive: if case_insensitive:
@@ -223,29 +251,32 @@ class RegexMatch():
except re.error: except re.error:
result = False result = False
return result, return io.NodeOutput(result)
class RegexExtract(): class RegexExtract(io.ComfyNode):
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="RegexExtract",
"string": (IO.STRING, {"multiline": True}), display_name="Regex Extract",
"regex_pattern": (IO.STRING, {"multiline": True}), category="utils/string",
"mode": (IO.COMBO, {"options": ["First Match", "All Matches", "First Group", "All Groups"]}), inputs=[
"case_insensitive": (IO.BOOLEAN, {"default": True}), io.String.Input("string", multiline=True),
"multiline": (IO.BOOLEAN, {"default": False}), io.String.Input("regex_pattern", multiline=True),
"dotall": (IO.BOOLEAN, {"default": False}), io.Combo.Input("mode", options=["First Match", "All Matches", "First Group", "All Groups"]),
"group_index": (IO.INT, {"default": 1, "min": 0, "max": 100}) io.Boolean.Input("case_insensitive", default=True),
} io.Boolean.Input("multiline", default=False),
} io.Boolean.Input("dotall", default=False),
io.Int.Input("group_index", default=1, min=0, max=100),
],
outputs=[
io.String.Output(),
]
)
RETURN_TYPES = (IO.STRING,) @classmethod
FUNCTION = "execute" def execute(cls, string, regex_pattern, mode, case_insensitive, multiline, dotall, group_index):
CATEGORY = "utils/string"
def execute(self, string, regex_pattern, mode, case_insensitive, multiline, dotall, group_index, **kwargs):
join_delimiter = "\n" join_delimiter = "\n"
flags = 0 flags = 0
@@ -294,32 +325,33 @@ class RegexExtract():
except re.error: except re.error:
result = "" result = ""
return result, return io.NodeOutput(result)
class RegexReplace(): class RegexReplace(io.ComfyNode):
DESCRIPTION = "Find and replace text using regex patterns."
@classmethod @classmethod
def INPUT_TYPES(s): def define_schema(cls):
return { return io.Schema(
"required": { node_id="RegexReplace",
"string": (IO.STRING, {"multiline": True}), display_name="Regex Replace",
"regex_pattern": (IO.STRING, {"multiline": True}), category="utils/string",
"replace": (IO.STRING, {"multiline": True}), description="Find and replace text using regex patterns.",
}, inputs=[
"optional": { io.String.Input("string", multiline=True),
"case_insensitive": (IO.BOOLEAN, {"default": True}), io.String.Input("regex_pattern", multiline=True),
"multiline": (IO.BOOLEAN, {"default": False}), io.String.Input("replace", multiline=True),
"dotall": (IO.BOOLEAN, {"default": False, "tooltip": "When enabled, the dot (.) character will match any character including newline characters. When disabled, dots won't match newlines."}), io.Boolean.Input("case_insensitive", default=True, optional=True),
"count": (IO.INT, {"default": 0, "min": 0, "max": 100, "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."}), io.Boolean.Input("multiline", default=False, optional=True),
} io.Boolean.Input("dotall", 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", 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(),
]
)
RETURN_TYPES = (IO.STRING,) @classmethod
FUNCTION = "execute" def execute(cls, string, regex_pattern, replace, case_insensitive=True, multiline=False, dotall=False, count=0):
CATEGORY = "utils/string"
def execute(self, string, regex_pattern, replace, case_insensitive=True, multiline=False, dotall=False, count=0, **kwargs):
flags = 0 flags = 0
if case_insensitive: if case_insensitive:
@@ -329,32 +361,25 @@ class RegexReplace():
if dotall: if dotall:
flags |= re.DOTALL flags |= re.DOTALL
result = re.sub(regex_pattern, replace, string, count=count, flags=flags) result = re.sub(regex_pattern, replace, string, count=count, flags=flags)
return result, 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 = { class StringExtension(ComfyExtension):
"StringConcatenate": "Concatenate", @override
"StringSubstring": "Substring", async def get_node_list(self) -> list[type[io.ComfyNode]]:
"StringLength": "Length", return [
"CaseConverter": "Case Converter", StringConcatenate,
"StringTrim": "Trim", StringSubstring,
"StringReplace": "Replace", StringLength,
"StringContains": "Contains", CaseConverter,
"StringCompare": "Compare", StringTrim,
"RegexMatch": "Regex Match", StringReplace,
"RegexExtract": "Regex Extract", StringContains,
"RegexReplace": "Regex Replace", StringCompare,
} RegexMatch,
RegexExtract,
RegexReplace,
]
async def comfy_entrypoint() -> StringExtension:
return StringExtension()