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:
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
@@ -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, "")
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user