Add string/path utility nodes (PathJoin, StringFormat, StringExtract, StringSwitch)
Some checks failed
Publish to Comfy registry / Publish Custom Node to registry (push) Has been cancelled
Some checks failed
Publish to Comfy registry / Publish Custom Node to registry (push) Has been cancelled
Four new nodes that collapse common multi-node chains for path construction, string templating, substring extraction, and boolean selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
52
README.md
52
README.md
@@ -112,6 +112,58 @@ This means segments don't need to be in order and can have gaps (e.g. 1, 5, 10).
|
||||
<img src="assets/segment-lookup.svg" alt="Segment lookup diagram showing batch_data selection" />
|
||||
</p>
|
||||
|
||||
## String & Path Utility Nodes
|
||||
|
||||
Four additional nodes that replace common multi-node chains for string and path operations.
|
||||
|
||||
### Path Join (`utils/path`)
|
||||
|
||||
Joins 1–6 path segments using `os.path.join` with automatic normalization.
|
||||
|
||||
| Input | Type | Notes |
|
||||
|:---|:---|:---|
|
||||
| `segment_1` | STRING | Required |
|
||||
| `segment_2` – `segment_6` | STRING | Optional |
|
||||
|
||||
**Output:** `path` (STRING)
|
||||
|
||||
### String Format (`utils/string`)
|
||||
|
||||
Python format-string templating (`{0}/{1:04d}.{2}`) with up to 8 connected inputs. Handles zero-padding, type conversion, and arbitrary templates in one node.
|
||||
|
||||
| Input | Type | Notes |
|
||||
|:---|:---|:---|
|
||||
| `template` | STRING | Format string, e.g. `{0}/{1:04d}` |
|
||||
| `v0` – `v7` | any | Optional values to substitute |
|
||||
|
||||
**Output:** `string` (STRING)
|
||||
|
||||
### String Extract (`utils/string`)
|
||||
|
||||
Extracts substrings with 3 modes:
|
||||
|
||||
| Mode | Description |
|
||||
|:---|:---|
|
||||
| `split_take` | Split on delimiter, take part at index (negative indices supported) |
|
||||
| `between` | Extract text between two delimiters |
|
||||
| `filename_parts` | Decompose path into dirname / basename / extension |
|
||||
|
||||
**Outputs:** `result`, `dirname`, `basename`, `extension` (all STRING)
|
||||
|
||||
### String Switch (`utils/string`)
|
||||
|
||||
Boolean-based selection with built-in default values.
|
||||
|
||||
| Input | Type | Notes |
|
||||
|:---|:---|:---|
|
||||
| `condition` | BOOLEAN | Required |
|
||||
| `on_true` / `on_false` | any | Optional connected values |
|
||||
| `default_true` / `default_false` | STRING | Fallback widget values |
|
||||
|
||||
**Output:** `result` (any)
|
||||
|
||||
When a connected input (`on_true`/`on_false`) is present it takes priority; otherwise the corresponding `default_*` string widget is used.
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](LICENSE)
|
||||
|
||||
12
__init__.py
12
__init__.py
@@ -1,4 +1,14 @@
|
||||
from .json_loader_dynamic import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .json_loader_dynamic import (
|
||||
NODE_CLASS_MAPPINGS as _json_class_mappings,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as _json_display_mappings,
|
||||
)
|
||||
from .string_utils import (
|
||||
NODE_CLASS_MAPPINGS as _string_class_mappings,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as _string_display_mappings,
|
||||
)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {**_json_class_mappings, **_string_class_mappings}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**_json_display_mappings, **_string_display_mappings}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-json-dynamic"
|
||||
version = "1.0.0"
|
||||
description = "A ComfyUI node that auto-discovers JSON keys and exposes them as typed output slots"
|
||||
description = "ComfyUI nodes for dynamic JSON loading and string/path utility operations"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.10"
|
||||
classifiers = ["Operating System :: OS Independent"]
|
||||
|
||||
170
string_utils.py
Normal file
170
string_utils.py
Normal file
@@ -0,0 +1,170 @@
|
||||
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,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"JDL_PathJoin": JDL_PathJoin,
|
||||
"JDL_StringFormat": JDL_StringFormat,
|
||||
"JDL_StringExtract": JDL_StringExtract,
|
||||
"JDL_StringSwitch": JDL_StringSwitch,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"JDL_PathJoin": "Path Join",
|
||||
"JDL_StringFormat": "String Format",
|
||||
"JDL_StringExtract": "String Extract",
|
||||
"JDL_StringSwitch": "String Switch",
|
||||
}
|
||||
Reference in New Issue
Block a user