Files
ComfyUI-JSON-Dynamic/string_utils.py
Ethanfel 0559e16cf0
Some checks are pending
Publish to Comfy registry / Publish Custom Node to registry (push) Waiting to run
Add Ordered Passthrough node with auto-wired execution order
JS extension auto-creates LiteGraph links between consecutive
passthrough nodes based on their order widget (1→2→3→4), hiding
the wait_for input and drawing visual chain lines between nodes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 03:28:04 +01:00

197 lines
6.0 KiB
Python

import os
class AnyType(str):
"""Universal connector type that matches any ComfyUI type."""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
class JDL_PathJoin:
"""Joins 1-6 path segments using os.path.join."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"segment_1": ("STRING", {"default": "", "multiline": False}),
},
"optional": {
"segment_2": ("STRING", {"default": "", "multiline": False}),
"segment_3": ("STRING", {"default": "", "multiline": False}),
"segment_4": ("STRING", {"default": "", "multiline": False}),
"segment_5": ("STRING", {"default": "", "multiline": False}),
"segment_6": ("STRING", {"default": "", "multiline": False}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("path",)
FUNCTION = "join_path"
CATEGORY = "utils/path"
def join_path(self, segment_1, segment_2="", segment_3="", segment_4="",
segment_5="", segment_6=""):
segments = [s for s in [segment_1, segment_2, segment_3, segment_4,
segment_5, segment_6] if s]
if not segments:
return ("",)
return (os.path.normpath(os.path.join(*segments)),)
class JDL_StringFormat:
"""Python format-string templating with up to 8 inputs."""
@classmethod
def INPUT_TYPES(s):
optional = {}
for i in range(8):
optional[f"v{i}"] = (any_type, {"default": ""})
return {
"required": {
"template": ("STRING", {"default": "{0}/{1}", "multiline": False}),
},
"optional": optional,
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string",)
FUNCTION = "format_string"
CATEGORY = "utils/string"
def format_string(self, template, **kwargs):
values = []
for i in range(8):
values.append(kwargs.get(f"v{i}", ""))
try:
result = template.format(*values)
except (IndexError, KeyError, ValueError) as e:
result = f"[format error: {e}]"
return (result,)
class JDL_StringExtract:
"""Extracts substrings via split, between-delimiters, or filename decomposition."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"default": "", "multiline": False}),
"mode": (["split_take", "between", "filename_parts"],),
},
"optional": {
"delimiter": ("STRING", {"default": "/", "multiline": False}),
"index": ("INT", {"default": -1, "min": -999, "max": 999}),
"delimiter_2": ("STRING", {"default": "", "multiline": False}),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("result", "dirname", "basename", "extension")
FUNCTION = "extract"
CATEGORY = "utils/string"
def extract(self, text, mode, delimiter="/", index=-1, delimiter_2=""):
dirname = os.path.dirname(text)
full_basename = os.path.basename(text)
name, ext = os.path.splitext(full_basename)
extension = ext.lstrip(".")
if mode == "split_take":
parts = text.split(delimiter)
try:
result = parts[index]
except IndexError:
result = ""
elif mode == "between":
result = ""
start = text.find(delimiter)
if start != -1:
start += len(delimiter)
if delimiter_2:
end = text.find(delimiter_2, start)
if end != -1:
result = text[start:end]
else:
result = text[start:]
else:
result = text[start:]
else: # filename_parts
result = name
return (result, dirname, name, extension)
class JDL_StringSwitch:
"""Boolean-based string selection with built-in defaults."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"condition": ("BOOLEAN", {"default": True}),
},
"optional": {
"on_true": (any_type,),
"on_false": (any_type,),
"default_true": ("STRING", {"default": "", "multiline": False}),
"default_false": ("STRING", {"default": "", "multiline": False}),
},
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("result",)
FUNCTION = "switch"
CATEGORY = "utils/string"
def switch(self, condition, on_true=None, on_false=None,
default_true="", default_false=""):
if condition:
return (on_true if on_true is not None else default_true,)
else:
return (on_false if on_false is not None else default_false,)
class JDL_DependencyPassthrough:
"""Passes data through unchanged. Set 'order' in the JS widget to auto-wire
execution order between passthrough nodes (1 → 2 → 3 → 4)."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"data": (any_type,),
},
"optional": {
"wait_for": (any_type,),
},
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("data",)
FUNCTION = "passthrough"
CATEGORY = "utils/flow"
def passthrough(self, data, wait_for=None):
return (data,)
NODE_CLASS_MAPPINGS = {
"JDL_PathJoin": JDL_PathJoin,
"JDL_StringFormat": JDL_StringFormat,
"JDL_StringExtract": JDL_StringExtract,
"JDL_StringSwitch": JDL_StringSwitch,
"JDL_DependencyPassthrough": JDL_DependencyPassthrough,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"JDL_PathJoin": "Path Join",
"JDL_StringFormat": "String Format",
"JDL_StringExtract": "String Extract",
"JDL_StringSwitch": "String Switch",
"JDL_DependencyPassthrough": "Ordered Passthrough",
}