import re from typing import Any, List _PLACEHOLDER_RE = re.compile( r"%(?:" r"%|" # escaped percent r"(?:[-+#0 ]*)?" # flags r"(?:\d+|\*)?" # width r"(?:\.(?:\d+|\*))?" # precision r"[hlL]?" # length modifier (ignored by Python for most) r"[diuoxXeEfFgGcrsa]" # conversion type r")" ) class CNPFormat: @classmethod def INPUT_TYPES(cls): return { "required": { "format": ( "STRING", { "default": "Size: %u x %u | Prompt: %s", "multiline": True, }, ), "arg_1": ("*", {}), } } RETURN_TYPES = ("STRING",) RETURN_NAMES = ("text",) FUNCTION = "format_text" CATEGORY = "ComfyNodesPlus/Text" @classmethod def VALIDATE_INPUTS(cls, input_types): return True def _ordered_args(self, kwargs: dict[str, Any]) -> List[Any]: args: List[Any] = [] index = 1 while True: key = f"arg_{index}" if key not in kwargs: break args.append(kwargs[key]) index += 1 return args def _normalise_value(self, value: Any, specifier: str) -> Any: if value is None: return "" if specifier == "s" else 0 if specifier == "s": return str(value) if specifier == "c": if isinstance(value, int): return value text = str(value) if len(text) != 1: raise ValueError(f"%c expects a single character, got {value!r}") return text if specifier in {"d", "i", "u", "o", "x", "X"}: if isinstance(value, bool): return int(value) if isinstance(value, (int, float)): return int(value) text = str(value).strip() if text == "": return 0 return int(float(text)) if specifier in {"e", "E", "f", "F", "g", "G"}: if isinstance(value, bool): return 1.0 if value else 0.0 if isinstance(value, (int, float)): return float(value) text = str(value).strip() if text == "": return 0.0 return float(text) return value def _extract_specifiers(self, fmt: str) -> List[str]: specifiers: List[str] = [] for match in _PLACEHOLDER_RE.finditer(fmt): token = match.group(0) if token == "%%": continue specifiers.append(token[-1]) return specifiers def format_text(self, format: str, **kwargs): args = self._ordered_args(kwargs) specifiers = self._extract_specifiers(format) if len(args) < len(specifiers): raise ValueError( f"CNPFormat: format string expects {len(specifiers)} argument(s), " f"but only {len(args)} were connected." ) converted = [] for index, specifier in enumerate(specifiers): converted.append(self._normalise_value(args[index], specifier)) try: if len(converted) == 0: result = format % () if "%%" in format else format elif len(converted) == 1: result = format % converted[0] else: result = format % tuple(converted) except Exception as exc: raise ValueError(f"CNPFormat failed to format string: {exc}") from exc return (str(result),)