Add StringContainsAny node for conditional workflows (Issue #9)

New node checks if input string contains any of multiple substrings,
returning a boolean and the first matched string. Useful for conditionally
loading LoRAs based on prompt keywords.
This commit is contained in:
BRADSEC
2026-01-31 10:12:52 +10:00
parent c8b45da72e
commit 0146015f1b
5 changed files with 180 additions and 3 deletions
+14
View File
@@ -8,6 +8,7 @@ Simple string manipulation nodes for ComfyUI (strip/remove text strings, search
- String Strip
- String Multi Replace
- String Conditional Append
- String Contains Any
- String Preview
***Once added nodes can be found under the `Add Node` > `utils` > `StringEssentials` menu.***
@@ -15,6 +16,7 @@ Simple string manipulation nodes for ComfyUI (strip/remove text strings, search
## Features
- Remove or replace text, words, numbers in input strings
- Conditionally append text only when not already present
- Check if text contains any of multiple substrings (for conditional workflows)
- Handle multi-word strings (partial sentences/phrases)
- Case-sensitive/insensitive matching
- Whole string matching option
@@ -68,6 +70,18 @@ Conditionally adds strings to the input text only if they're not already present
- **`output_string`**: The resulting text (with or without appended strings)
- **`was_appended`**: Boolean indicating whether any strings were added
## String Contains Any
Checks if the input string contains any of the specified substrings. Returns a boolean result and the first matched substring. Useful for conditional workflows, such as loading specific LoRAs based on prompt keywords (e.g., load anime LoRA if prompt contains "anime", "manga", or "cel-shaded").
### Inputs
- **`input_string`**: The string or text to search within (connection input)
- **`substrings`**: List of substrings to search for (one per line)
- **`match_case`**: Enable/disable case-sensitive search (default: disabled)
### Outputs
- **`contains_any`**: Boolean indicating whether any substring was found
- **`matched_string`**: The first substring that was found (empty string if none matched)
## String Preview
Simply displays a string output. Can be used to view the results/changes to the input_string by the `String Strip` or `String Replace`.
+5 -2
View File
@@ -5,13 +5,15 @@ from .string_strip_node import StringStripNode
from .string_multi_replace_node import StringMultiReplaceNode
from .string_preview_node import StringPreviewNode
from .string_conditional_append_node import StringConditionalAppendNode
from .string_contains_any_node import StringContainsAnyNode
NODE_CLASS_MAPPINGS = {
"StringTextbox": StringTextboxNode,
"StringStrip": StringStripNode,
"StringMultiReplace": StringMultiReplaceNode,
"StringPreview": StringPreviewNode,
"StringConditionalAppend": StringConditionalAppendNode
"StringConditionalAppend": StringConditionalAppendNode,
"StringContainsAny": StringContainsAnyNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -19,7 +21,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"StringStrip": "String Strip",
"StringMultiReplace": "String Multi Replace",
"StringPreview": "String Preview",
"StringConditionalAppend": "String Conditional Append"
"StringConditionalAppend": "String Conditional Append",
"StringContainsAny": "String Contains Any"
}
WEB_DIRECTORY = "./js"
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_stringessentials"
description = "Simple string manipulation nodes for ComfyUI (strip/remove text strings, search and replace text strings, preview modified string outputs). Useful for modifying text prompts or results from LLM outputs. Nodes will be located under standard Add Node > utils menu. Node names: String Textbox, String Strip, String Multi Replace, String Conditional Append and String Preview."
version = "2.0.4"
version = "2.0.5"
license = {file = "LICENSE"}
[project.urls]
+52
View File
@@ -0,0 +1,52 @@
class StringContainsAnyNode:
"""Check if an input string contains any of multiple substrings."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_string": ("STRING", {
"forceInput": True,
"tooltip": "The string to search within"
}),
"substrings": ("STRING", {
"multiline": True,
"default": "",
"placeholder": "Enter substrings to search for (one per line)",
"tooltip": "Enter substrings to check (one per line). Returns true if ANY are found."
}),
"match_case": ("BOOLEAN", {
"default": False,
"label_on": "enabled",
"label_off": "disabled",
"tooltip": "Whether the search should be case-sensitive"
})
}
}
RETURN_TYPES = ("BOOLEAN", "STRING")
RETURN_NAMES = ("contains_any", "matched_string")
FUNCTION = "contains_any"
CATEGORY = "utils/StringEssentials"
def contains_any(self, input_string, substrings, match_case):
# Parse substrings, one per line
substrings_list = []
for line in substrings.splitlines():
line = line.strip()
if line:
substrings_list.append(line)
if not substrings_list:
return (False, "")
# Prepare strings for comparison
search_string = input_string if match_case else input_string.lower()
# Check each substring
for substring in substrings_list:
check_substring = substring if match_case else substring.lower()
if check_substring in search_string:
return (True, substring)
return (False, "")
+108
View File
@@ -2,12 +2,14 @@ import unittest
from string_strip_node import StringStripNode
from string_multi_replace_node import StringMultiReplaceNode
from string_conditional_append_node import StringConditionalAppendNode
from string_contains_any_node import StringContainsAnyNode
class TestStringNodes(unittest.TestCase):
def setUp(self):
self.strip_node = StringStripNode()
self.replace_node = StringMultiReplaceNode()
self.append_node = StringConditionalAppendNode()
self.contains_any_node = StringContainsAnyNode()
def test_strip_basic_functionality(self):
input_string = "This is a test string with test word"
@@ -404,5 +406,111 @@ goodbye"""
)[0]
self.assertEqual(result, "column1\tcolumn2\tcolumn3")
# Tests for StringContainsAnyNode (Issue #9)
def test_contains_any_found(self):
# Substring present, returns True
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful anime portrait",
substrings="anime",
match_case=False
)
self.assertTrue(result)
self.assertEqual(matched, "anime")
def test_contains_any_not_found(self):
# No match, returns False
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful landscape",
substrings="anime",
match_case=False
)
self.assertFalse(result)
self.assertEqual(matched, "")
def test_contains_any_multiple_first_match(self):
# Returns first matched substring
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful anime manga portrait",
substrings="""cel-shaded
anime
manga""",
match_case=False
)
self.assertTrue(result)
self.assertEqual(matched, "anime")
def test_contains_any_case_insensitive(self):
# Default case-insensitive behavior
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful ANIME portrait",
substrings="anime",
match_case=False
)
self.assertTrue(result)
self.assertEqual(matched, "anime")
def test_contains_any_case_sensitive(self):
# Case-sensitive matching
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful ANIME portrait",
substrings="anime",
match_case=True
)
self.assertFalse(result)
self.assertEqual(matched, "")
def test_contains_any_case_sensitive_found(self):
# Case-sensitive matching with exact case
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful ANIME portrait",
substrings="ANIME",
match_case=True
)
self.assertTrue(result)
self.assertEqual(matched, "ANIME")
def test_contains_any_empty_input(self):
# Empty input string
result, matched = self.contains_any_node.contains_any(
input_string="",
substrings="anime",
match_case=False
)
self.assertFalse(result)
self.assertEqual(matched, "")
def test_contains_any_empty_substrings(self):
# Empty substrings
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful anime portrait",
substrings="",
match_case=False
)
self.assertFalse(result)
self.assertEqual(matched, "")
def test_contains_any_whitespace_substrings(self):
# Substrings with only whitespace lines (should be ignored)
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful anime portrait",
substrings="""
anime
""",
match_case=False
)
self.assertTrue(result)
self.assertEqual(matched, "anime")
def test_contains_any_partial_match(self):
# Partial substring match (anime matches animation)
result, matched = self.contains_any_node.contains_any(
input_string="A beautiful animation",
substrings="anim",
match_case=False
)
self.assertTrue(result)
self.assertEqual(matched, "anim")
if __name__ == '__main__':
unittest.main()