Skip to content

Commit 4936d01

Browse files
committed
string node
1 parent d9c80a8 commit 4936d01

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed

comfy_extras/nodes_string.py

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import re
2+
3+
from comfy.comfy_types.node_typing import IO
4+
5+
def normalize_path(path):
6+
return path.replace('\\', '/')
7+
8+
class StringConcatenate():
9+
@classmethod
10+
def INPUT_TYPES(s):
11+
return {
12+
"required": {
13+
"string_a": (IO.STRING, {"multiline": True}),
14+
"string_b": (IO.STRING, {"multiline": True})
15+
},
16+
"optional": {
17+
"separator": (IO.STRING, {"default": ""})
18+
}
19+
}
20+
21+
RETURN_TYPES = (IO.STRING,)
22+
FUNCTION = "execute"
23+
CATEGORY = "utils/string"
24+
25+
def execute(self, string_a, string_b, separator="", **kwargs):
26+
return string_a + separator + string_b,
27+
28+
class StringSubstring():
29+
@classmethod
30+
def INPUT_TYPES(s):
31+
return {
32+
"required": {
33+
"string": (IO.STRING, {"multiline": True}),
34+
"start": (IO.INT, {}),
35+
"end": (IO.INT, {}),
36+
}
37+
}
38+
39+
RETURN_TYPES = (IO.STRING,)
40+
FUNCTION = "execute"
41+
CATEGORY = "utils/string"
42+
43+
def execute(self, string, start, end, **kwargs):
44+
return string[start:end],
45+
46+
class StringLength():
47+
@classmethod
48+
def INPUT_TYPES(s):
49+
return {
50+
"required": {
51+
"string": (IO.STRING, {"multiline": True})
52+
}
53+
}
54+
55+
RETURN_TYPES = (IO.INT,)
56+
RETURN_NAMES = ("length",)
57+
FUNCTION = "execute"
58+
CATEGORY = "utils/string"
59+
60+
def execute(self, string, **kwargs):
61+
length = len(string)
62+
63+
return length,
64+
65+
class CaseConverter():
66+
@classmethod
67+
def INPUT_TYPES(s):
68+
return {
69+
"required": {
70+
"string": (IO.STRING, {"multiline": True}),
71+
"mode": (["UPPERCASE", "lowercase", "Capitalize", "Title Case"], {})
72+
}
73+
}
74+
75+
RETURN_TYPES = (IO.STRING,)
76+
FUNCTION = "execute"
77+
CATEGORY = "utils/string"
78+
79+
def execute(self, string, mode, **kwargs):
80+
if mode == "UPPERCASE":
81+
result = string.upper()
82+
elif mode == "lowercase":
83+
result = string.lower()
84+
elif mode == "Capitalize":
85+
result = string.capitalize()
86+
elif mode == "Title Case":
87+
result = string.title()
88+
else:
89+
result = string
90+
91+
return result,
92+
93+
94+
class StringTrim():
95+
@classmethod
96+
def INPUT_TYPES(s):
97+
return {
98+
"required": {
99+
"string": (IO.STRING, {"multiline": True}),
100+
"mode": (["BOTH", "LEFT", "RIGHT"], {})
101+
}
102+
}
103+
104+
RETURN_TYPES = (IO.STRING,)
105+
FUNCTION = "execute"
106+
CATEGORY = "utils/string"
107+
108+
def execute(self, string, mode, **kwargs):
109+
if mode == "BOTH":
110+
result = string.strip()
111+
elif mode == "LEFT":
112+
result = string.lstrip()
113+
elif mode == "RIGHT":
114+
result = string.rstrip()
115+
else:
116+
result = string
117+
118+
return result,
119+
120+
class StringReplace():
121+
@classmethod
122+
def INPUT_TYPES(s):
123+
return {
124+
"required": {
125+
"string": (IO.STRING, {"multiline": True}),
126+
"find": (IO.STRING, {"multiline": True}),
127+
"replace": (IO.STRING, {"multiline": True})
128+
}
129+
}
130+
131+
RETURN_TYPES = (IO.STRING,)
132+
FUNCTION = "execute"
133+
CATEGORY = "utils/string"
134+
135+
def execute(self, string, find, replace, **kwargs):
136+
result = string.replace(find, replace)
137+
return result,
138+
139+
140+
class StringContains():
141+
@classmethod
142+
def INPUT_TYPES(s):
143+
return {
144+
"required": {
145+
"string": (IO.STRING, {"multiline": True}),
146+
"substring": (IO.STRING, {"multiline": True}),
147+
"case_sensitive": (IO.BOOLEAN, {"default": True})
148+
}
149+
}
150+
151+
RETURN_TYPES = (IO.BOOLEAN,)
152+
RETURN_NAMES = ("contains",)
153+
FUNCTION = "execute"
154+
CATEGORY = "utils/string"
155+
156+
def execute(self, string, substring, case_sensitive, **kwargs):
157+
if case_sensitive:
158+
contains = substring in string
159+
else:
160+
contains = substring.lower() in string.lower()
161+
162+
return contains,
163+
164+
165+
class StringCompare():
166+
@classmethod
167+
def INPUT_TYPES(s):
168+
return {
169+
"required": {
170+
"string_a": (IO.STRING, {"multiline": True}),
171+
"string_b": (IO.STRING, {"multiline": True}),
172+
"mode": (["StartsWith", "EndsWith", "Equal"], {"default": "StartsWith"}),
173+
"case_sensitive": (IO.BOOLEAN, {"default": True})
174+
}
175+
}
176+
177+
RETURN_TYPES = (IO.BOOLEAN,)
178+
FUNCTION = "execute"
179+
CATEGORY = "utils/string"
180+
181+
def execute(self, string_a, string_b, mode, case_sensitive, **kwargs):
182+
if case_sensitive:
183+
a = string_a
184+
b = string_b
185+
else:
186+
a = string_a.lower()
187+
b = string_b.lower()
188+
189+
if mode == "Equal":
190+
return a == b,
191+
elif mode == "StartsWith":
192+
return a.startswith(b),
193+
elif mode == "EndsWith":
194+
return a.endswith(b),
195+
196+
class RegexMatch():
197+
@classmethod
198+
def INPUT_TYPES(s):
199+
return {
200+
"required": {
201+
"string": (IO.STRING, {"multiline": True}),
202+
"regex_pattern": (IO.STRING, {"multiline": True}),
203+
"case_insensitive": (IO.BOOLEAN, {"default": True}),
204+
"multiline": (IO.BOOLEAN, {"default": False}),
205+
"dotall": (IO.BOOLEAN, {"default": False})
206+
}
207+
}
208+
209+
RETURN_TYPES = (IO.BOOLEAN,)
210+
RETURN_NAMES = ("matches",)
211+
FUNCTION = "execute"
212+
CATEGORY = "utils/string"
213+
214+
def execute(self, string, regex_pattern, case_insensitive,
215+
multiline, dotall, **kwargs):
216+
flags = 0
217+
218+
if case_insensitive:
219+
flags |= re.IGNORECASE
220+
if multiline:
221+
flags |= re.MULTILINE
222+
if dotall:
223+
flags |= re.DOTALL
224+
225+
try:
226+
match = re.search(regex_pattern, string, flags)
227+
result = match is not None
228+
229+
except re.error:
230+
result = False
231+
232+
return result,
233+
234+
235+
class RegexExtract():
236+
@classmethod
237+
def INPUT_TYPES(s):
238+
return {
239+
"required": {
240+
"string": (IO.STRING, {"multiline": True}),
241+
"regex_pattern": (IO.STRING, {"multiline": True}),
242+
"mode": (["First Match", "All Matches", "First Group", "All Groups"], {}),
243+
"case_insensitive": (IO.BOOLEAN, {"default": True}),
244+
"multiline": (IO.BOOLEAN, {"default": False}),
245+
"dotall": (IO.BOOLEAN, {"default": False}),
246+
"group_index": (IO.INT, {"default": 1, "min": 0, "max": 100})
247+
}
248+
}
249+
250+
RETURN_TYPES = (IO.STRING,)
251+
FUNCTION = "execute"
252+
CATEGORY = "utils/string"
253+
254+
def execute(self, string, regex_pattern, mode, case_insensitive,
255+
multiline, dotall, group_index, **kwargs):
256+
257+
join_delimiter = "\n"
258+
259+
flags = 0
260+
if case_insensitive:
261+
flags |= re.IGNORECASE
262+
if multiline:
263+
flags |= re.MULTILINE
264+
if dotall:
265+
flags |= re.DOTALL
266+
267+
try:
268+
if mode == "First Match":
269+
match = re.search(regex_pattern, string, flags)
270+
if match:
271+
result = match.group(0)
272+
else:
273+
result = ""
274+
275+
elif mode == "All Matches":
276+
matches = re.findall(regex_pattern, string, flags)
277+
if matches:
278+
if isinstance(matches[0], tuple):
279+
result = join_delimiter.join([m[0] for m in matches])
280+
else:
281+
result = join_delimiter.join(matches)
282+
else:
283+
result = ""
284+
285+
elif mode == "First Group":
286+
match = re.search(regex_pattern, string, flags)
287+
if match and len(match.groups()) >= group_index:
288+
result = match.group(group_index)
289+
else:
290+
result = ""
291+
292+
elif mode == "All Groups":
293+
matches = re.finditer(regex_pattern, string, flags)
294+
results = []
295+
for match in matches:
296+
if match.groups() and len(match.groups()) >= group_index:
297+
results.append(match.group(group_index))
298+
result = join_delimiter.join(results)
299+
else:
300+
result = ""
301+
302+
except re.error:
303+
result = ""
304+
305+
return result,
306+
307+
NODE_CLASS_MAPPINGS = {
308+
"StringConcatenate": StringConcatenate,
309+
"StringSubstring": StringSubstring,
310+
"StringLength": StringLength,
311+
"CaseConverter": CaseConverter,
312+
"StringTrim": StringTrim,
313+
"StringReplace": StringReplace,
314+
"StringContains": StringContains,
315+
"StringCompare": StringCompare,
316+
"RegexMatch": RegexMatch,
317+
"RegexExtract": RegexExtract
318+
}
319+
320+
NODE_DISPLAY_NAME_MAPPINGS = {
321+
"StringConcatenate": "Concatenate",
322+
"StringSubstring": "Substring",
323+
"StringLength": "Length",
324+
"CaseConverter": "Case Converter",
325+
"StringTrim": "Trim",
326+
"StringReplace": "Replace",
327+
"StringContains": "Contains",
328+
"StringCompare": "Compare",
329+
"RegexMatch": "Regex Match",
330+
"RegexExtract": "Regex Extract"
331+
}

nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2259,6 +2259,7 @@ def init_builtin_extra_nodes():
22592259
"nodes_hidream.py",
22602260
"nodes_fresca.py",
22612261
"nodes_preview_any.py",
2262+
"nodes_string.py",
22622263
]
22632264

22642265
api_nodes_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_api_nodes")

0 commit comments

Comments
 (0)