Skip to content

Commit b7ed5f5

Browse files
authored
string node (#7952)
1 parent b4abca8 commit b7ed5f5

File tree

2 files changed

+323
-0
lines changed

2 files changed

+323
-0
lines changed

comfy_extras/nodes_string.py

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

nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2263,6 +2263,7 @@ def init_builtin_extra_nodes():
22632263
"nodes_fresca.py",
22642264
"nodes_preview_any.py",
22652265
"nodes_ace.py",
2266+
"nodes_string.py",
22662267
]
22672268

22682269
import_failed = []

0 commit comments

Comments
 (0)