From 0d9dfd2e05b03d4bdeb8fae6b62cb8bfb81cdd27 Mon Sep 17 00:00:00 2001 From: MaraScott Date: Thu, 28 Aug 2025 12:35:38 +0200 Subject: [PATCH 01/17] Update node_formatter.py to accept curly bracket in template for non variables in dict --- node_formatter.py | 235 +++++++++++++++++++++++++++------------------- 1 file changed, 141 insertions(+), 94 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index ce2210d..ef6f7d6 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -1,125 +1,172 @@ # encoding: utf-8 """ +Updated StringConstructorFormatter with template sanitization """ import typing as _t - +import re as _re from inspect import cleandoc as _cleandoc import sys as _sys from frozendict import deepfreeze as _deepfreeze - from comfy.comfy_types.node_typing import IO as _IO from . import _meta from .docstring_formatter import format_docstring as _format_docstring from .enums import DataTypes as _DataTypes from .funcs_common import _show_text_on_node, _verify_input_dict -# from .funcs_formatter import formatter as _formatter +_RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) -_RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈ +def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: + """ + Safe format that only replaces variables that exist in the dictionary. + Leaves other curly brackets untouched. + """ + if not format_dict: + return template + + # Create pattern that matches {key} only for keys that exist in format_dict + pattern = r'\{(' + '|'.join(_re.escape(key) for key in format_dict.keys()) + r')\}' + + def replace_func(match): + key = match.group(1) + return str(format_dict[key]) + + return _re.sub(pattern, replace_func, template) +def _escape_unknown_brackets(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: + """ + Escape curly brackets that aren't format variables, then use normal format. + This approach preserves the original behavior for known variables. + """ + if not format_dict: + # If no format dict, escape all curly brackets + return template.replace('{', '{{').replace('}', '}}') + + # Find all {word} patterns + pattern = r'\{([^{}]*)\}' + known_keys = set(format_dict.keys()) + + def escape_func(match): + key = match.group(1) + if key in known_keys: + return match.group(0) # Keep as is for formatting + else: + return '{{' + key + '}}' # Escape unknown + + escaped_template = _re.sub(pattern, escape_func, template) + return escaped_template.format_map(format_dict) -def _recursive_format(template: str, format_dict: _t.Dict[str, _t.Any], show: bool = True, unique_id: str = None) -> str: - """ - It's not actually recursive - because, you know, any recursion could be turned into iteration, - and good boys do that. 😊 - """ - assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 +def _recursive_format_safe(template: str, format_dict: _t.Dict[str, _t.Any], show: bool = True, unique_id: str = None, use_safe_mode: bool = True) -> str: + """ + Safe recursive format that won't break on unknown curly brackets. + """ + assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 - prev: str = '' - new: str = template - for i in range(_RECURSION_LIMIT): - if prev == new: - break - prev = new - new = new.format_map(format_dict) - if prev == new: - return new + prev: str = '' + new: str = template + + for i in range(_RECURSION_LIMIT): + if prev == new: + break + prev = new + + if use_safe_mode: + new = _safe_format(new, format_dict) + else: + # Use the escape approach for compatibility with original behavior + new = _escape_unknown_brackets(new, format_dict) + + if prev == new: + return new - msg = ( - f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n" - f"Last two formatting attempts:\n{prev!r}\n{new!r}" - ) - if show and unique_id: - _show_text_on_node(msg, unique_id) - raise RecursionError(msg) - # noinspection PyUnreachableCode - return '' # just to be safe - -# -------------------------------------- - -_dict = dict + msg = ( + f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n" + f"Last two formatting attempts:\n{prev!r}\n{new!r}" + ) + if show and unique_id: + _show_text_on_node(msg, unique_id) + raise RecursionError(msg) +# Updated input types with new sanitization option _input_types = _deepfreeze({ - 'required': { - 'template': (_IO.STRING, {'multiline': True, 'tooltip': ( - "Type the text template. " - "To reference named substrings from format-dictionary, use this syntax: {substring_name}. For example:\n\n" - "score_9, score_8_up, score_7_up, {char1_short}, standing next to {char2_short},\n" - "{char1_long}\n{char2_long}" - )}), - 'recursive_format': (_IO.BOOLEAN, {'default': False, 'label_on': '❗ yes', 'label_off': 'no', 'tooltip': ( - "Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks - which in turn " - "lets you do really crazy stuff like building entire HIERARCHIES of sub-prompts, all linked to the same " - "wording typed in one place.\n\n" - "The exclamation mark reminds you that with great power comes great responsibility!\n" - "You can end up with chunks cross-referencing each other in a loop. In such case, the node will just error out " - "and won't let you crash the entire ComfyUI - but still, it's your responsibility to prevent " - "such looping dictionaries." - )}), - 'show_status': (_IO.BOOLEAN, {'default': True, 'label_on': 'formatted string', 'label_off': 'no', 'tooltip': ( - "Show the final string constructed from the text-template and format-dictionary?" - )}), - }, - 'optional': { - 'dict': _DataTypes.input_dict(tooltip=( - "The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference " - "any sub-strings - then, this node acts exactly the same as a regular string-primitive node." - )), - }, - 'hidden': { - 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node - }, + 'required': { + 'template': (_IO.STRING, {'multiline': True, 'tooltip': ( + "Type the text template. " + "To reference named substrings from format-dictionary, use this syntax: {substring_name}. For example:\n\n" + "score_9, score_8_up, score_7_up, {char1_short}, standing next to {char2_short},\n" + "{char1_long}\n{char2_long}\n\n" + "Curly brackets that don't match dictionary keys will be handled according to the sanitization mode." + )}), + 'recursive_format': (_IO.BOOLEAN, {'default': False, 'label_on': '❗ yes', 'label_off': 'no', 'tooltip': ( + "Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks." + )}), + 'safe_mode': (_IO.BOOLEAN, {'default': True, 'label_on': 'safe', 'label_off': 'escape', 'tooltip': ( + "Safe mode: Only format known variables, leave unknown {brackets} as-is.\n" + "Escape mode: Escape unknown {brackets} to {{brackets}} then format normally.\n" + "Safe mode is recommended for templates with JSON, CSS, or other literal curly brackets." + )}), + 'show_status': (_IO.BOOLEAN, {'default': True, 'label_on': 'formatted string', 'label_off': 'no', 'tooltip': ( + "Show the final string constructed from the text-template and format-dictionary?" + )}), + }, + 'optional': { + 'dict': _DataTypes.input_dict(tooltip=( + "The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference " + "any sub-strings - then, this node acts exactly the same as a regular string-primitive node." + )), + }, + 'hidden': { + 'unique_id': 'UNIQUE_ID', + }, }) - class StringConstructorFormatter: - """ - Construct the formatted string from template and format-dictionary. - """ - NODE_NAME = 'StringConstructorFormatter' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + """ + Construct the formatted string from template and format-dictionary. + Now with safe handling of curly brackets that aren't format variables. + """ + NODE_NAME = 'StringConstructorFormatter' + CATEGORY = _meta.category + DESCRIPTION = _format_docstring(_cleandoc(__doc__)) - OUTPUT_NODE = True + OUTPUT_NODE = True - FUNCTION = 'main' - RETURN_TYPES = (_IO.STRING, ) - RETURN_NAMES = ('string', ) - # OUTPUT_TOOLTIPS = tuple() + FUNCTION = 'main' + RETURN_TYPES = (_IO.STRING, ) + RETURN_NAMES = ('string', ) - @classmethod - def INPUT_TYPES(cls): - return _input_types + @classmethod + def INPUT_TYPES(cls): + return _input_types - @staticmethod - def main( - template: str, recursive_format: bool = False, show_status: bool = False, dict: _t.Dict[str, _t.Any] = None, - unique_id: str = None - ) -> _t.Tuple[str]: - if dict is None: - dict = _dict() - _verify_input_dict(dict) - if not isinstance(template, str): - raise TypeError(f"Not a string: {template!r}") + @staticmethod + def main( + template: str, + recursive_format: bool = False, + safe_mode: bool = True, + show_status: bool = False, + dict: _t.Dict[str, _t.Any] = None, + unique_id: str = None + ) -> _t.Tuple[str]: + + if dict is None: + dict = {} + _verify_input_dict(dict) + + if not isinstance(template, str): + raise TypeError(f"Not a string: {template!r}") - out_text = ( - _recursive_format(template, dict, show_status, unique_id) - if recursive_format - else template.format_map(dict) - ) - if show_status and unique_id: - _show_text_on_node(out_text, unique_id) - return (out_text, ) + if recursive_format: + out_text = _recursive_format_safe(template, dict, show_status, unique_id, safe_mode) + else: + if safe_mode: + out_text = _safe_format(template, dict) + else: + out_text = _escape_unknown_brackets(template, dict) + + if show_status and unique_id: + _show_text_on_node(out_text, unique_id) + + return (out_text, ) From c2524445e877cc1d5f41f95a2618fcc80bbf2ad3 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:17:08 +0500 Subject: [PATCH 02/17] restore: spaces -> tabs --- node_formatter.py | 268 +++++++++++++++++++++++----------------------- 1 file changed, 134 insertions(+), 134 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index ef6f7d6..a8b4eb8 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -19,154 +19,154 @@ from .funcs_common import _show_text_on_node, _verify_input_dict _RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: - """ - Safe format that only replaces variables that exist in the dictionary. - Leaves other curly brackets untouched. - """ - if not format_dict: - return template - - # Create pattern that matches {key} only for keys that exist in format_dict - pattern = r'\{(' + '|'.join(_re.escape(key) for key in format_dict.keys()) + r')\}' - - def replace_func(match): - key = match.group(1) - return str(format_dict[key]) - - return _re.sub(pattern, replace_func, template) + """ + Safe format that only replaces variables that exist in the dictionary. + Leaves other curly brackets untouched. + """ + if not format_dict: + return template + + # Create pattern that matches {key} only for keys that exist in format_dict + pattern = r'\{(' + '|'.join(_re.escape(key) for key in format_dict.keys()) + r')\}' + + def replace_func(match): + key = match.group(1) + return str(format_dict[key]) + + return _re.sub(pattern, replace_func, template) def _escape_unknown_brackets(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: - """ - Escape curly brackets that aren't format variables, then use normal format. - This approach preserves the original behavior for known variables. - """ - if not format_dict: - # If no format dict, escape all curly brackets - return template.replace('{', '{{').replace('}', '}}') - - # Find all {word} patterns - pattern = r'\{([^{}]*)\}' - known_keys = set(format_dict.keys()) - - def escape_func(match): - key = match.group(1) - if key in known_keys: - return match.group(0) # Keep as is for formatting - else: - return '{{' + key + '}}' # Escape unknown - - escaped_template = _re.sub(pattern, escape_func, template) - return escaped_template.format_map(format_dict) + """ + Escape curly brackets that aren't format variables, then use normal format. + This approach preserves the original behavior for known variables. + """ + if not format_dict: + # If no format dict, escape all curly brackets + return template.replace('{', '{{').replace('}', '}}') + + # Find all {word} patterns + pattern = r'\{([^{}]*)\}' + known_keys = set(format_dict.keys()) + + def escape_func(match): + key = match.group(1) + if key in known_keys: + return match.group(0) # Keep as is for formatting + else: + return '{{' + key + '}}' # Escape unknown + + escaped_template = _re.sub(pattern, escape_func, template) + return escaped_template.format_map(format_dict) def _recursive_format_safe(template: str, format_dict: _t.Dict[str, _t.Any], show: bool = True, unique_id: str = None, use_safe_mode: bool = True) -> str: - """ - Safe recursive format that won't break on unknown curly brackets. - """ - assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 + """ + Safe recursive format that won't break on unknown curly brackets. + """ + assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 - prev: str = '' - new: str = template - - for i in range(_RECURSION_LIMIT): - if prev == new: - break - prev = new - - if use_safe_mode: - new = _safe_format(new, format_dict) - else: - # Use the escape approach for compatibility with original behavior - new = _escape_unknown_brackets(new, format_dict) - - if prev == new: - return new + prev: str = '' + new: str = template + + for i in range(_RECURSION_LIMIT): + if prev == new: + break + prev = new + + if use_safe_mode: + new = _safe_format(new, format_dict) + else: + # Use the escape approach for compatibility with original behavior + new = _escape_unknown_brackets(new, format_dict) + + if prev == new: + return new - msg = ( - f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n" - f"Last two formatting attempts:\n{prev!r}\n{new!r}" - ) - if show and unique_id: - _show_text_on_node(msg, unique_id) - raise RecursionError(msg) + msg = ( + f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n" + f"Last two formatting attempts:\n{prev!r}\n{new!r}" + ) + if show and unique_id: + _show_text_on_node(msg, unique_id) + raise RecursionError(msg) # Updated input types with new sanitization option _input_types = _deepfreeze({ - 'required': { - 'template': (_IO.STRING, {'multiline': True, 'tooltip': ( - "Type the text template. " - "To reference named substrings from format-dictionary, use this syntax: {substring_name}. For example:\n\n" - "score_9, score_8_up, score_7_up, {char1_short}, standing next to {char2_short},\n" - "{char1_long}\n{char2_long}\n\n" - "Curly brackets that don't match dictionary keys will be handled according to the sanitization mode." - )}), - 'recursive_format': (_IO.BOOLEAN, {'default': False, 'label_on': '❗ yes', 'label_off': 'no', 'tooltip': ( - "Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks." - )}), - 'safe_mode': (_IO.BOOLEAN, {'default': True, 'label_on': 'safe', 'label_off': 'escape', 'tooltip': ( - "Safe mode: Only format known variables, leave unknown {brackets} as-is.\n" - "Escape mode: Escape unknown {brackets} to {{brackets}} then format normally.\n" - "Safe mode is recommended for templates with JSON, CSS, or other literal curly brackets." - )}), - 'show_status': (_IO.BOOLEAN, {'default': True, 'label_on': 'formatted string', 'label_off': 'no', 'tooltip': ( - "Show the final string constructed from the text-template and format-dictionary?" - )}), - }, - 'optional': { - 'dict': _DataTypes.input_dict(tooltip=( - "The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference " - "any sub-strings - then, this node acts exactly the same as a regular string-primitive node." - )), - }, - 'hidden': { - 'unique_id': 'UNIQUE_ID', - }, + 'required': { + 'template': (_IO.STRING, {'multiline': True, 'tooltip': ( + "Type the text template. " + "To reference named substrings from format-dictionary, use this syntax: {substring_name}. For example:\n\n" + "score_9, score_8_up, score_7_up, {char1_short}, standing next to {char2_short},\n" + "{char1_long}\n{char2_long}\n\n" + "Curly brackets that don't match dictionary keys will be handled according to the sanitization mode." + )}), + 'recursive_format': (_IO.BOOLEAN, {'default': False, 'label_on': '❗ yes', 'label_off': 'no', 'tooltip': ( + "Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks." + )}), + 'safe_mode': (_IO.BOOLEAN, {'default': True, 'label_on': 'safe', 'label_off': 'escape', 'tooltip': ( + "Safe mode: Only format known variables, leave unknown {brackets} as-is.\n" + "Escape mode: Escape unknown {brackets} to {{brackets}} then format normally.\n" + "Safe mode is recommended for templates with JSON, CSS, or other literal curly brackets." + )}), + 'show_status': (_IO.BOOLEAN, {'default': True, 'label_on': 'formatted string', 'label_off': 'no', 'tooltip': ( + "Show the final string constructed from the text-template and format-dictionary?" + )}), + }, + 'optional': { + 'dict': _DataTypes.input_dict(tooltip=( + "The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference " + "any sub-strings - then, this node acts exactly the same as a regular string-primitive node." + )), + }, + 'hidden': { + 'unique_id': 'UNIQUE_ID', + }, }) class StringConstructorFormatter: - """ - Construct the formatted string from template and format-dictionary. - Now with safe handling of curly brackets that aren't format variables. - """ - NODE_NAME = 'StringConstructorFormatter' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + """ + Construct the formatted string from template and format-dictionary. + Now with safe handling of curly brackets that aren't format variables. + """ + NODE_NAME = 'StringConstructorFormatter' + CATEGORY = _meta.category + DESCRIPTION = _format_docstring(_cleandoc(__doc__)) - OUTPUT_NODE = True + OUTPUT_NODE = True - FUNCTION = 'main' - RETURN_TYPES = (_IO.STRING, ) - RETURN_NAMES = ('string', ) + FUNCTION = 'main' + RETURN_TYPES = (_IO.STRING, ) + RETURN_NAMES = ('string', ) - @classmethod - def INPUT_TYPES(cls): - return _input_types + @classmethod + def INPUT_TYPES(cls): + return _input_types - @staticmethod - def main( - template: str, - recursive_format: bool = False, - safe_mode: bool = True, - show_status: bool = False, - dict: _t.Dict[str, _t.Any] = None, - unique_id: str = None - ) -> _t.Tuple[str]: - - if dict is None: - dict = {} - _verify_input_dict(dict) - - if not isinstance(template, str): - raise TypeError(f"Not a string: {template!r}") + @staticmethod + def main( + template: str, + recursive_format: bool = False, + safe_mode: bool = True, + show_status: bool = False, + dict: _t.Dict[str, _t.Any] = None, + unique_id: str = None + ) -> _t.Tuple[str]: + + if dict is None: + dict = {} + _verify_input_dict(dict) + + if not isinstance(template, str): + raise TypeError(f"Not a string: {template!r}") - if recursive_format: - out_text = _recursive_format_safe(template, dict, show_status, unique_id, safe_mode) - else: - if safe_mode: - out_text = _safe_format(template, dict) - else: - out_text = _escape_unknown_brackets(template, dict) - - if show_status and unique_id: - _show_text_on_node(out_text, unique_id) - - return (out_text, ) + if recursive_format: + out_text = _recursive_format_safe(template, dict, show_status, unique_id, safe_mode) + else: + if safe_mode: + out_text = _safe_format(template, dict) + else: + out_text = _escape_unknown_brackets(template, dict) + + if show_status and unique_id: + _show_text_on_node(out_text, unique_id) + + return (out_text, ) From 8893bf3e7b7b739ad10f1cb8522201b7fa89c4d2 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:27:28 +0500 Subject: [PATCH 03/17] + remove trailing spaces --- node_formatter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index a8b4eb8..f4b5394 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -143,10 +143,10 @@ class StringConstructorFormatter: @staticmethod def main( - template: str, - recursive_format: bool = False, + template: str, + recursive_format: bool = False, safe_mode: bool = True, - show_status: bool = False, + show_status: bool = False, dict: _t.Dict[str, _t.Any] = None, unique_id: str = None ) -> _t.Tuple[str]: From 7158842e3c4d28355588c64dd50dc21143ac3da4 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 29 Aug 2025 08:38:29 +0500 Subject: [PATCH 04/17] Restore comments and changes about code style --- node_formatter.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index f4b5394..5a7a5e3 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -1,14 +1,16 @@ # encoding: utf-8 """ -Updated StringConstructorFormatter with template sanitization +Code for ``StringConstructorFormatter`` node. """ import typing as _t -import re as _re + from inspect import cleandoc as _cleandoc +import re as _re import sys as _sys from frozendict import deepfreeze as _deepfreeze + from comfy.comfy_types.node_typing import IO as _IO from . import _meta @@ -16,7 +18,9 @@ from .docstring_formatter import format_docstring as _format_docstring from .enums import DataTypes as _DataTypes from .funcs_common import _show_text_on_node, _verify_input_dict -_RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) + +_RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈ + def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: """ @@ -35,6 +39,7 @@ def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: return _re.sub(pattern, replace_func, template) + def _escape_unknown_brackets(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: """ Escape curly brackets that aren't format variables, then use normal format. @@ -58,15 +63,21 @@ def _escape_unknown_brackets(template: str, format_dict: _t.Dict[str, _t.Any]) - escaped_template = _re.sub(pattern, escape_func, template) return escaped_template.format_map(format_dict) -def _recursive_format_safe(template: str, format_dict: _t.Dict[str, _t.Any], show: bool = True, unique_id: str = None, use_safe_mode: bool = True) -> str: + +def _recursive_format_safe( + template: str, format_dict: _t.Dict[str, _t.Any], + show: bool = True, unique_id: str = None, use_safe_mode: bool = True +) -> str: """ Safe recursive format that won't break on unknown curly brackets. + + It's not actually recursive - because, you know, any recursion could be turned into iteration, + and good boys do that. 😊 """ assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 prev: str = '' new: str = template - for i in range(_RECURSION_LIMIT): if prev == new: break @@ -88,8 +99,11 @@ def _recursive_format_safe(template: str, format_dict: _t.Dict[str, _t.Any], sho if show and unique_id: _show_text_on_node(msg, unique_id) raise RecursionError(msg) + # noinspection PyUnreachableCode + return '' # just to be safe + +# -------------------------------------- -# Updated input types with new sanitization option _input_types = _deepfreeze({ 'required': { 'template': (_IO.STRING, {'multiline': True, 'tooltip': ( @@ -118,14 +132,14 @@ _input_types = _deepfreeze({ )), }, 'hidden': { - 'unique_id': 'UNIQUE_ID', + 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node }, }) + class StringConstructorFormatter: """ Construct the formatted string from template and format-dictionary. - Now with safe handling of curly brackets that aren't format variables. """ NODE_NAME = 'StringConstructorFormatter' CATEGORY = _meta.category @@ -136,6 +150,7 @@ class StringConstructorFormatter: FUNCTION = 'main' RETURN_TYPES = (_IO.STRING, ) RETURN_NAMES = ('string', ) + # OUTPUT_TOOLTIPS = tuple() @classmethod def INPUT_TYPES(cls): @@ -150,11 +165,9 @@ class StringConstructorFormatter: dict: _t.Dict[str, _t.Any] = None, unique_id: str = None ) -> _t.Tuple[str]: - if dict is None: dict = {} _verify_input_dict(dict) - if not isinstance(template, str): raise TypeError(f"Not a string: {template!r}") @@ -168,5 +181,4 @@ class StringConstructorFormatter: if show_status and unique_id: _show_text_on_node(out_text, unique_id) - return (out_text, ) From ebb03f393d491f4c47f2f59b29855bb3c9116096 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:24:18 +0500 Subject: [PATCH 05/17] Change args order --- node_formatter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index 5a7a5e3..c89a838 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -65,8 +65,8 @@ def _escape_unknown_brackets(template: str, format_dict: _t.Dict[str, _t.Any]) - def _recursive_format_safe( - template: str, format_dict: _t.Dict[str, _t.Any], - show: bool = True, unique_id: str = None, use_safe_mode: bool = True + template: str, format_dict: _t.Dict[str, _t.Any], use_safe_mode: bool = True, + show: bool = True, unique_id: str = None ) -> str: """ Safe recursive format that won't break on unknown curly brackets. @@ -126,7 +126,7 @@ _input_types = _deepfreeze({ )}), }, 'optional': { - 'dict': _DataTypes.input_dict(tooltip=( + 'dict': _DataTypes.input_dict(tooltip=( # It's not actually optional, but is here since there's no dict-widget "The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference " "any sub-strings - then, this node acts exactly the same as a regular string-primitive node." )), @@ -162,7 +162,7 @@ class StringConstructorFormatter: recursive_format: bool = False, safe_mode: bool = True, show_status: bool = False, - dict: _t.Dict[str, _t.Any] = None, + dict: _t.Dict[str, _t.Any] = None, #actually, required - but here to keep the declared params order unique_id: str = None ) -> _t.Tuple[str]: if dict is None: @@ -172,7 +172,7 @@ class StringConstructorFormatter: raise TypeError(f"Not a string: {template!r}") if recursive_format: - out_text = _recursive_format_safe(template, dict, show_status, unique_id, safe_mode) + out_text = _recursive_format_safe(template, dict, safe_mode, show_status, unique_id) else: if safe_mode: out_text = _safe_format(template, dict) From 072e098837c2d5c9901ea0a479d3c4cdefe39da7 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:46:10 +0500 Subject: [PATCH 06/17] pre-cache regex --- node_formatter.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index c89a838..374ff89 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -37,10 +37,15 @@ def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: key = match.group(1) return str(format_dict[key]) - return _re.sub(pattern, replace_func, template) + return _re.sub(pattern, replace_func, template) # TODO: turn to a pre-compiled func -def _escape_unknown_brackets(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: +_re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-replace func to find and replace {keyword} patterns + r'\{([^{}]*)\}' +).sub + + +def _escape_unknown_keywords(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: """ Escape curly brackets that aren't format variables, then use normal format. This approach preserves the original behavior for known variables. @@ -49,23 +54,21 @@ def _escape_unknown_brackets(template: str, format_dict: _t.Dict[str, _t.Any]) - # If no format dict, escape all curly brackets return template.replace('{', '{{').replace('}', '}}') - # Find all {word} patterns - pattern = r'\{([^{}]*)\}' known_keys = set(format_dict.keys()) - def escape_func(match): + def escape_func(match: _re.Match[str]) -> str: # TODO: extract to define only once (and not in the loop) key = match.group(1) if key in known_keys: return match.group(0) # Keep as is for formatting else: return '{{' + key + '}}' # Escape unknown - escaped_template = _re.sub(pattern, escape_func, template) + escaped_template = _re_formatting_keyword_sub(escape_func, template) return escaped_template.format_map(format_dict) def _recursive_format_safe( - template: str, format_dict: _t.Dict[str, _t.Any], use_safe_mode: bool = True, + template: str, format_dict: _t.Dict[str, _t.Any], safe_mode: bool = True, show: bool = True, unique_id: str = None ) -> str: """ @@ -76,18 +79,16 @@ def _recursive_format_safe( """ assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 + # Avoid unnecessary conditions in the loop: + formatting_func = _safe_format if safe_mode else _escape_unknown_keywords # TODO: the latter one doesn't actually format + prev: str = '' new: str = template for i in range(_RECURSION_LIMIT): if prev == new: break prev = new - - if use_safe_mode: - new = _safe_format(new, format_dict) - else: - # Use the escape approach for compatibility with original behavior - new = _escape_unknown_brackets(new, format_dict) + new = formatting_func(new, format_dict) if prev == new: return new @@ -100,7 +101,7 @@ def _recursive_format_safe( _show_text_on_node(msg, unique_id) raise RecursionError(msg) # noinspection PyUnreachableCode - return '' # just to be safe + return '' # just to be extra-safe, if RecursionError is treated as warning # -------------------------------------- @@ -177,7 +178,7 @@ class StringConstructorFormatter: if safe_mode: out_text = _safe_format(template, dict) else: - out_text = _escape_unknown_brackets(template, dict) + out_text = _escape_unknown_keywords(template, dict) if show_status and unique_id: _show_text_on_node(out_text, unique_id) From 142705a3cb28962fa30e6a44c2e3a021fd4fb7f5 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:51:41 +0500 Subject: [PATCH 07/17] reorder funcs --- node_formatter.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index 374ff89..7f0296b 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -22,24 +22,6 @@ from .funcs_common import _show_text_on_node, _verify_input_dict _RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈ -def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: - """ - Safe format that only replaces variables that exist in the dictionary. - Leaves other curly brackets untouched. - """ - if not format_dict: - return template - - # Create pattern that matches {key} only for keys that exist in format_dict - pattern = r'\{(' + '|'.join(_re.escape(key) for key in format_dict.keys()) + r')\}' - - def replace_func(match): - key = match.group(1) - return str(format_dict[key]) - - return _re.sub(pattern, replace_func, template) # TODO: turn to a pre-compiled func - - _re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-replace func to find and replace {keyword} patterns r'\{([^{}]*)\}' ).sub @@ -67,6 +49,24 @@ def _escape_unknown_keywords(template: str, format_dict: _t.Dict[str, _t.Any]) - return escaped_template.format_map(format_dict) +def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: + """ + Safe format that only replaces variables that exist in the dictionary. + Leaves other curly brackets untouched. + """ + if not format_dict: + return template + + # Create pattern that matches {key} only for keys that exist in format_dict + pattern = r'\{(' + '|'.join(_re.escape(key) for key in format_dict.keys()) + r')\}' + + def replace_func(match): + key = match.group(1) + return str(format_dict[key]) + + return _re.sub(pattern, replace_func, template) # TODO: turn to a pre-compiled func + + def _recursive_format_safe( template: str, format_dict: _t.Dict[str, _t.Any], safe_mode: bool = True, show: bool = True, unique_id: str = None From d286e07f709a8e161523d7148873984cd818fc9c Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:20:58 +0500 Subject: [PATCH 08/17] match_escaping_func as an argument --- node_formatter.py | 52 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index 7f0296b..12d7505 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -5,6 +5,7 @@ Code for ``StringConstructorFormatter`` node. import typing as _t +from functools import partial as _partial from inspect import cleandoc as _cleandoc import re as _re import sys as _sys @@ -22,12 +23,32 @@ from .funcs_common import _show_text_on_node, _verify_input_dict _RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈ +_t_esc_func = _t.Callable[[_re.Match[str]], str] + + +def _build_match_escaping_func_for_re_sub(known_keys: _t.Iterable[str]) -> _t_esc_func: + """Function factory: builds a replacing func for a specific set of dict keys - to be passed to ``re.sub()``.""" + known_keys_set = set(known_keys) + assert all(isinstance(x, str) for x in known_keys_set), f"Dict keys should be pre-validated by now. Got: {known_keys_set!r}" + + def escape_func(match: _re.Match[str]) -> str: + key = match.group(1) + if key in known_keys_set: + return match.group(0) # Keep as is for formatting + else: + return '{{' + key + '}}' # Escape unknown + + return escape_func + + _re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-replace func to find and replace {keyword} patterns - r'\{([^{}]*)\}' + r'\{([^{}]*)\}' # TODO: this won't catch repeating braces. Account for that. ).sub -def _escape_unknown_keywords(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: +def _escape_unknown_keywords( + template: str, format_dict: _t.Dict[str, _t.Any], match_escaping_func: _t_esc_func +) -> str: """ Escape curly brackets that aren't format variables, then use normal format. This approach preserves the original behavior for known variables. @@ -36,18 +57,10 @@ def _escape_unknown_keywords(template: str, format_dict: _t.Dict[str, _t.Any]) - # If no format dict, escape all curly brackets return template.replace('{', '{{').replace('}', '}}') - known_keys = set(format_dict.keys()) - - def escape_func(match: _re.Match[str]) -> str: # TODO: extract to define only once (and not in the loop) - key = match.group(1) - if key in known_keys: - return match.group(0) # Keep as is for formatting - else: - return '{{' + key + '}}' # Escape unknown - - escaped_template = _re_formatting_keyword_sub(escape_func, template) + escaped_template = _re_formatting_keyword_sub(match_escaping_func, template) return escaped_template.format_map(format_dict) +# -------------------------------------- def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: """ @@ -79,8 +92,14 @@ def _recursive_format_safe( """ assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 - # Avoid unnecessary conditions in the loop: - formatting_func = _safe_format if safe_mode else _escape_unknown_keywords # TODO: the latter one doesn't actually format + # Avoid unnecessary `if`s in the loop - pre-build a condition-less func: + if safe_mode: + formatting_func = _safe_format + else: + formatting_func = _partial( + _escape_unknown_keywords, + match_escaping_func=_build_match_escaping_func_for_re_sub(set(format_dict.keys())) + ) prev: str = '' new: str = template @@ -178,7 +197,10 @@ class StringConstructorFormatter: if safe_mode: out_text = _safe_format(template, dict) else: - out_text = _escape_unknown_keywords(template, dict) + out_text = _escape_unknown_keywords( + template, dict, + match_escaping_func=_build_match_escaping_func_for_re_sub(set(dict.keys())) + ) if show_status and unique_id: _show_text_on_node(out_text, unique_id) From a08e42f394b496c7608a69e7cd95217e59c76cbd Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:03:04 +0500 Subject: [PATCH 09/17] `node_formatter`: **COMPLETE REFACTOR** - `_Formatter` class introduced instead of a bunch of functions for all the cases. --- node_formatter.py | 212 ++++++++++++++++++++++++---------------------- 1 file changed, 111 insertions(+), 101 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index 12d7505..cdfe4ef 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -5,7 +5,7 @@ Code for ``StringConstructorFormatter`` node. import typing as _t -from functools import partial as _partial +from dataclasses import dataclass as _dataclass from inspect import cleandoc as _cleandoc import re as _re import sys as _sys @@ -22,105 +22,128 @@ from .funcs_common import _show_text_on_node, _verify_input_dict _RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈ - -_t_esc_func = _t.Callable[[_re.Match[str]], str] - - -def _build_match_escaping_func_for_re_sub(known_keys: _t.Iterable[str]) -> _t_esc_func: - """Function factory: builds a replacing func for a specific set of dict keys - to be passed to ``re.sub()``.""" - known_keys_set = set(known_keys) - assert all(isinstance(x, str) for x in known_keys_set), f"Dict keys should be pre-validated by now. Got: {known_keys_set!r}" - - def escape_func(match: _re.Match[str]) -> str: - key = match.group(1) - if key in known_keys_set: - return match.group(0) # Keep as is for formatting - else: - return '{{' + key + '}}' # Escape unknown - - return escape_func - - -_re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-replace func to find and replace {keyword} patterns +_re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-sub func to find and replace {keyword} patterns r'\{([^{}]*)\}' # TODO: this won't catch repeating braces. Account for that. ).sub -def _escape_unknown_keywords( - template: str, format_dict: _t.Dict[str, _t.Any], match_escaping_func: _t_esc_func -) -> str: +@_dataclass +class _Formatter: """ - Escape curly brackets that aren't format variables, then use normal format. - This approach preserves the original behavior for known variables. + A callable (function-like) class, which does the actual formatting, while respecting all the options. + + It's made as a class to split the formatting into two stages: + - First, the instance is properly initialized with the shared arguments (all the method overrides are made depending on options); + - Then, the actual instance is treated as a function - it needs to be called with the formatted string as the only argument. + + It's done this way to avoid extra conditions in the loop + to organize all the setup work in one place. """ - if not format_dict: - # If no format dict, escape all curly brackets - return template.replace('{', '{{').replace('}', '}}') - - escaped_template = _re_formatting_keyword_sub(match_escaping_func, template) - return escaped_template.format_map(format_dict) + format_dict: _t.Optional[_t.Dict[str, _t.Any], None] = None -# -------------------------------------- + recursive: bool = False + safe: bool = True -def _safe_format(template: str, format_dict: _t.Dict[str, _t.Any]) -> str: - """ - Safe format that only replaces variables that exist in the dictionary. - Leaves other curly brackets untouched. - """ - if not format_dict: - return template + show_status: bool = True + unique_node_id: str = None - # Create pattern that matches {key} only for keys that exist in format_dict - pattern = r'\{(' + '|'.join(_re.escape(key) for key in format_dict.keys()) + r')\}' + def __post_init__(self): # called by dataclass init + if self.format_dict is None: + self.format_dict = dict() - def replace_func(match): + format_dict = self.format_dict + _verify_input_dict(format_dict) + if not format_dict: + self.__escape_match = self.__escape_match_no_keys + + # No need to override __escape_for_safe_format(). + # Instead, recursive method defines local function, and simple method just checks self.safe mode. + + self._format = self.__format_recursive if self.recursive else self.__format_simple + + # @staticmethod + # def __dummy_return_intact(template: str) -> str: + # return template + + def __escape_match(self, match: _re.Match[str]) -> str: key = match.group(1) - return str(format_dict[key]) + if key in self.format_dict: + return match.group(0) # Keep as is for formatting + else: + return '{{' + key + '}}' # Escape unknown - return _re.sub(pattern, replace_func, template) # TODO: turn to a pre-compiled func + @staticmethod + def __escape_match_no_keys(match: _re.Match[str]) -> str: + key = match.group(1) + return '{{' + key + '}}' # Escape unknown + def __escape_for_safe_format(self, template: str) -> str: + """ + Method to be called on template before the actual format - in safe mode. -def _recursive_format_safe( - template: str, format_dict: _t.Dict[str, _t.Any], safe_mode: bool = True, - show: bool = True, unique_id: str = None -) -> str: - """ - Safe recursive format that won't break on unknown curly brackets. + Safe mode only replaces variables that exist in the dictionary. Leaves other curly brackets untouched. + """ + return _re_formatting_keyword_sub(self.__escape_match, template) - It's not actually recursive - because, you know, any recursion could be turned into iteration, - and good boys do that. 😊 - """ - assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 + def __format_simple(self, template: str) -> str: + if self.safe: + template = self.__escape_for_safe_format(template) + return template.format_map(self.format_dict) - # Avoid unnecessary `if`s in the loop - pre-build a condition-less func: - if safe_mode: - formatting_func = _safe_format - else: - formatting_func = _partial( - _escape_unknown_keywords, - match_escaping_func=_build_match_escaping_func_for_re_sub(set(format_dict.keys())) - ) + def __format_recursive(self, template: str) -> str: + """ + It's not actually recursive - because, you know, any recursion could be turned into iteration, + and good boys do that. 😊 + """ + assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 + + # Cache before loop - to avoid even 'dot' operator + escape_func = self.__escape_for_safe_format + format_dict: _t.Dict[str, _t.Any] = self.format_dict + + # The following two funcs defined here instead of method overrides - to simplify their local scope: + + def format_single_with_escape(_template: str): + _template = escape_func(_template) + return _template.format_map(format_dict) + + def format_single_no_escape(_template: str): + return _template.format_map(format_dict) + + format_single_func = format_single_with_escape if self.safe else format_single_no_escape + + prev: str = '' + new: str = template + for i in range(_RECURSION_LIMIT): + if prev == new: + break + prev = new + new = format_single_func(new) - prev: str = '' - new: str = template - for i in range(_RECURSION_LIMIT): if prev == new: - break - prev = new - new = formatting_func(new, format_dict) - - if prev == new: - return new + return new - msg = ( - f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n" - f"Last two formatting attempts:\n{prev!r}\n{new!r}" - ) - if show and unique_id: - _show_text_on_node(msg, unique_id) - raise RecursionError(msg) - # noinspection PyUnreachableCode - return '' # just to be extra-safe, if RecursionError is treated as warning + msg = ( + f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n" + f"Last two formatting attempts:\n{prev!r}\n{new!r}" + ) + if self.show_status and self.unique_node_id: + _show_text_on_node(msg, self.unique_node_id) + raise RecursionError(msg) + # noinspection PyUnreachableCode + return '' # just to be extra-safe, if RecursionError is treated as warning + + @staticmethod + def _format(template: str) -> str: + raise NotImplementedError("This method should always be overridden after initialization.") + + def __call__(self, template: str) -> str: + if not isinstance(template, str): + raise TypeError(f"Not a string: {template!r}") + + out_text = self._format(template) if template else '' + if self.show_status and self.unique_node_id: + _show_text_on_node(out_text, self.unique_node_id) + return out_text # -------------------------------------- @@ -182,26 +205,13 @@ class StringConstructorFormatter: recursive_format: bool = False, safe_mode: bool = True, show_status: bool = False, - dict: _t.Dict[str, _t.Any] = None, #actually, required - but here to keep the declared params order + dict: _t.Dict[str, _t.Any] = None, #actually, required - but it's here to keep the declared params order unique_id: str = None ) -> _t.Tuple[str]: - if dict is None: - dict = {} - _verify_input_dict(dict) - if not isinstance(template, str): - raise TypeError(f"Not a string: {template!r}") - - if recursive_format: - out_text = _recursive_format_safe(template, dict, safe_mode, show_status, unique_id) - else: - if safe_mode: - out_text = _safe_format(template, dict) - else: - out_text = _escape_unknown_keywords( - template, dict, - match_escaping_func=_build_match_escaping_func_for_re_sub(set(dict.keys())) - ) - - if show_status and unique_id: - _show_text_on_node(out_text, unique_id) + formatter = _Formatter( + format_dict=dict, + recursive=recursive_format, safe=safe_mode, + show_status=show_status, unique_node_id=unique_id, + ) + out_text = formatter(template) return (out_text, ) From 200dff7c81a2e21075d5cb939f37fbfac632bb1f Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:24:22 +0500 Subject: [PATCH 10/17] + account for leading/trailing brackets --- node_formatter.py | 65 +++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index cdfe4ef..e81737f 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -22,8 +22,29 @@ from .funcs_common import _show_text_on_node, _verify_input_dict _RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈ +def _n_brackets_after_escape_for_existing_key(n_brackets: int): + """ + Calculate the updated number of braces before/after the pattern. + Keeps the innermost set of brackets as-is for formatting, + but doubles all the leading/trailing brackets to escape them. + """ + n_extra = max(n_brackets - 1, 0) + return n_extra * 2 + 1 + + +def _rebuild_parsed_keyword(n_opening_brackets: int, inside_brackets: str, n_closing_brackets: int) -> str: + # According to "Beautiful/ideomatic python" lecture, string formatting is faster than other forms of concatenation + return '{}{}{}'.format( + '{' * n_opening_brackets, + inside_brackets, + '}' * n_closing_brackets + ) + + _re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-sub func to find and replace {keyword} patterns - r'\{([^{}]*)\}' # TODO: this won't catch repeating braces. Account for that. + '(\{+)([^{}]*)(\}+)' + # TODO: Takes leading/trailing braces into account, but not nested ones. Maybe implement some day, + # but it would require true parsing - with stack of braces, nested parts iterator, etc. ).sub @@ -55,8 +76,8 @@ class _Formatter: if not format_dict: self.__escape_match = self.__escape_match_no_keys - # No need to override __escape_for_safe_format(). - # Instead, recursive method defines local function, and simple method just checks self.safe mode. + # No need to override _escape_for_safe_format(). + # Instead, recursive method defines local function, and simple method just checks self.safe mode in-place. self._format = self.__format_recursive if self.recursive else self.__format_simple @@ -65,20 +86,22 @@ class _Formatter: # return template def __escape_match(self, match: _re.Match[str]) -> str: - key = match.group(1) - if key in self.format_dict: - return match.group(0) # Keep as is for formatting + opening_brackets, inside_brackets, closing_brackets = match.groups() + if inside_brackets.strip() in self.format_dict: + n_opening = _n_brackets_after_escape_for_existing_key(opening_brackets) + n_closing = _n_brackets_after_escape_for_existing_key(closing_brackets) + return _rebuild_parsed_keyword(n_opening, inside_brackets, n_closing) else: - return '{{' + key + '}}' # Escape unknown + return _rebuild_parsed_keyword(len(opening_brackets) * 2, inside_brackets, len(closing_brackets) * 2) @staticmethod def __escape_match_no_keys(match: _re.Match[str]) -> str: - key = match.group(1) - return '{{' + key + '}}' # Escape unknown + opening_brackets, inside_brackets, closing_brackets = match.groups() + return _rebuild_parsed_keyword(len(opening_brackets) * 2, inside_brackets, len(closing_brackets) * 2) - def __escape_for_safe_format(self, template: str) -> str: + def _escape_for_safe_format(self, template: str) -> str: """ - Method to be called on template before the actual format - in safe mode. + Method to be called in safe mode - on template, before the actual format. Safe mode only replaces variables that exist in the dictionary. Leaves other curly brackets untouched. """ @@ -86,7 +109,7 @@ class _Formatter: def __format_simple(self, template: str) -> str: if self.safe: - template = self.__escape_for_safe_format(template) + template = self._escape_for_safe_format(template) return template.format_map(self.format_dict) def __format_recursive(self, template: str) -> str: @@ -97,7 +120,7 @@ class _Formatter: assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 # Cache before loop - to avoid even 'dot' operator - escape_func = self.__escape_for_safe_format + escape_func = self._escape_for_safe_format format_dict: _t.Dict[str, _t.Any] = self.format_dict # The following two funcs defined here instead of method overrides - to simplify their local scope: @@ -115,13 +138,10 @@ class _Formatter: new: str = template for i in range(_RECURSION_LIMIT): if prev == new: - break + return new prev = new new = format_single_func(new) - if prev == new: - return new - msg = ( f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n" f"Last two formatting attempts:\n{prev!r}\n{new!r}" @@ -159,9 +179,10 @@ _input_types = _deepfreeze({ 'recursive_format': (_IO.BOOLEAN, {'default': False, 'label_on': '❗ yes', 'label_off': 'no', 'tooltip': ( "Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks." )}), - 'safe_mode': (_IO.BOOLEAN, {'default': True, 'label_on': 'safe', 'label_off': 'escape', 'tooltip': ( - "Safe mode: Only format known variables, leave unknown {brackets} as-is.\n" - "Escape mode: Escape unknown {brackets} to {{brackets}} then format normally.\n" + 'safe_format': (_IO.BOOLEAN, {'default': True, 'label_on': 'yes', 'label_off': 'no', 'tooltip': ( + "Safe mode: If a specific {keyword} doesn't exist in the dict, leave it as-is.\n" + "On: missing {keywords} preserved intact.\n" + "Off: missing {keywords} raise an error.\n\n" "Safe mode is recommended for templates with JSON, CSS, or other literal curly brackets." )}), 'show_status': (_IO.BOOLEAN, {'default': True, 'label_on': 'formatted string', 'label_off': 'no', 'tooltip': ( @@ -203,14 +224,14 @@ class StringConstructorFormatter: def main( template: str, recursive_format: bool = False, - safe_mode: bool = True, + safe_format: bool = True, show_status: bool = False, dict: _t.Dict[str, _t.Any] = None, #actually, required - but it's here to keep the declared params order unique_id: str = None ) -> _t.Tuple[str]: formatter = _Formatter( format_dict=dict, - recursive=recursive_format, safe=safe_mode, + recursive=recursive_format, safe=safe_format, show_status=show_status, unique_node_id=unique_id, ) out_text = formatter(template) From f764fefe7657f2d7fa04a711dcf6a21dd180485d Mon Sep 17 00:00:00 2001 From: MaraScott Date: Fri, 29 Aug 2025 14:09:52 +0200 Subject: [PATCH 11/17] fix(string-constructor): resolve Optional typing crash and regex warnings - Fixed TypeError caused by invalid use of typing.Optional with two arguments. Replaced Optional[Dict[str, Any], None] with correct Optional[Dict[str, Any]] = None. - Updated __escape_match() to pass integer brace counts instead of raw strings to _n_brackets_after_escape_for_existing_key, preventing runtime errors. - Converted regex patterns to raw strings (r\'...\') to eliminate SyntaxWarning about invalid escape sequences (\\s, \\{). --- docstring_formatter.py | 18 +++++++++--------- node_formatter.py | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docstring_formatter.py b/docstring_formatter.py index b8a7ede..c3359f0 100644 --- a/docstring_formatter.py +++ b/docstring_formatter.py @@ -8,16 +8,16 @@ from inspect import getdoc as _getdoc import re as _re -_re_indent_match = _re.compile("(\t*)( +)(\t*)(.*?)$").match -_re_tab_indent_match = _re.compile("(\t+)(.*?)$").match +_re_indent_match = _re.compile(r"(\t*)( +)(\t*)(.*?)$").match +_re_tab_indent_match = _re.compile(r"(\t+)(.*?)$").match _re_list_line_match = _re.compile( - "(\s*)(" - "[-*β€’]+" - "|" - "[a-zA-Z]\s*[.)]" - "|" - "[0-9+]\s*[.)]" - ")\s+" + r"(\s*)(" + r"[-*β€’]+" + r"|" + r"[a-zA-Z]\s*[.)]" + r"|" + r"[0-9+]\s*[.)]" + r")\s+" ).match diff --git a/node_formatter.py b/node_formatter.py index e81737f..9d359ad 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -42,7 +42,7 @@ def _rebuild_parsed_keyword(n_opening_brackets: int, inside_brackets: str, n_clo _re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-sub func to find and replace {keyword} patterns - '(\{+)([^{}]*)(\}+)' + r'(\{+)([^{}]*)(\}+)' # TODO: Takes leading/trailing braces into account, but not nested ones. Maybe implement some day, # but it would require true parsing - with stack of braces, nested parts iterator, etc. ).sub @@ -59,7 +59,7 @@ class _Formatter: It's done this way to avoid extra conditions in the loop + to organize all the setup work in one place. """ - format_dict: _t.Optional[_t.Dict[str, _t.Any], None] = None + format_dict: _t.Optional[_t.Dict[str, _t.Any]] = None recursive: bool = False safe: bool = True @@ -88,8 +88,8 @@ class _Formatter: def __escape_match(self, match: _re.Match[str]) -> str: opening_brackets, inside_brackets, closing_brackets = match.groups() if inside_brackets.strip() in self.format_dict: - n_opening = _n_brackets_after_escape_for_existing_key(opening_brackets) - n_closing = _n_brackets_after_escape_for_existing_key(closing_brackets) + n_opening = _n_brackets_after_escape_for_existing_key(len(opening_brackets)) + n_closing = _n_brackets_after_escape_for_existing_key(len(closing_brackets)) return _rebuild_parsed_keyword(n_opening, inside_brackets, n_closing) else: return _rebuild_parsed_keyword(len(opening_brackets) * 2, inside_brackets, len(closing_brackets) * 2) From e15988a53c500ebec4b747d070132254e588293a Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sat, 30 Aug 2025 05:30:11 +0500 Subject: [PATCH 12/17] Again, fully reworked the logic: instead of parsing and escaping the entire template ourself, let's rely on the built-in formatting and fallback on errors instead --- node_formatter.py | 184 +++++++++++++++++++++++++--------------------- 1 file changed, 100 insertions(+), 84 deletions(-) diff --git a/node_formatter.py b/node_formatter.py index 9d359ad..6ade2ef 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -5,7 +5,7 @@ Code for ``StringConstructorFormatter`` node. import typing as _t -from dataclasses import dataclass as _dataclass +from dataclasses import dataclass as _dataclass, field as _field from inspect import cleandoc as _cleandoc import re as _re import sys as _sys @@ -20,44 +20,30 @@ from .enums import DataTypes as _DataTypes from .funcs_common import _show_text_on_node, _verify_input_dict -_RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈ +_RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault πŸ€·πŸ»β€β™‚οΈsingle -def _n_brackets_after_escape_for_existing_key(n_brackets: int): - """ - Calculate the updated number of braces before/after the pattern. - Keeps the innermost set of brackets as-is for formatting, - but doubles all the leading/trailing brackets to escape them. - """ - n_extra = max(n_brackets - 1, 0) - return n_extra * 2 + 1 +__dataclass_slots_args = dict() if _sys.version_info < (3, 10) else dict(slots=True) + +_re_formatting_keyword_match = _re.compile( # Pre-compiled regex match to extract ``{keyword}`` patterns + r'(?P.*?)' + r'(?P\{+)' + r'(?P[^{}]+)' + r'(?P\}+)' + r'(?P[^{}].*)?$' +).match -def _rebuild_parsed_keyword(n_opening_brackets: int, inside_brackets: str, n_closing_brackets: int) -> str: - # According to "Beautiful/ideomatic python" lecture, string formatting is faster than other forms of concatenation - return '{}{}{}'.format( - '{' * n_opening_brackets, - inside_brackets, - '}' * n_closing_brackets - ) - - -_re_formatting_keyword_sub = _re.compile( # Pre-compiled regex-sub func to find and replace {keyword} patterns - r'(\{+)([^{}]*)(\}+)' - # TODO: Takes leading/trailing braces into account, but not nested ones. Maybe implement some day, - # but it would require true parsing - with stack of braces, nested parts iterator, etc. -).sub - - -@_dataclass +@_dataclass(**__dataclass_slots_args) class _Formatter: """ A callable (function-like) class, which does the actual formatting, while respecting all the options. It's made as a class to split the formatting into two stages: - - First, the instance is properly initialized with the shared arguments (all the method overrides are made depending on options); + - First, the instance is properly initialized with the shared arguments (the methods to call are conditionally assigned depending on options); - Then, the actual instance is treated as a function - it needs to be called with the formatted string as the only argument. - It's done this way to avoid extra conditions in the loop + to organize all the setup work in one place. + It's done this way to avoid extra conditions in the loop + to organize the convoluted mess of intertwined functions + into a more readable code. """ format_dict: _t.Optional[_t.Dict[str, _t.Any]] = None @@ -67,51 +53,97 @@ class _Formatter: show_status: bool = True unique_node_id: str = None + __format_single: _t.Callable[[str], str] = _field(init=False, repr=False, compare=False, default=lambda x: x) + _format: _t.Callable[[str], str] = _field(init=False, repr=False, compare=False, default=lambda x: x) + def __post_init__(self): # called by dataclass init if self.format_dict is None: self.format_dict = dict() format_dict = self.format_dict _verify_input_dict(format_dict) - if not format_dict: - self.__escape_match = self.__escape_match_no_keys + self.__format_single = self.__format_single_safe if self.safe else self.__format_single_unsafe - # No need to override _escape_for_safe_format(). - # Instead, recursive method defines local function, and simple method just checks self.safe mode in-place. - - self._format = self.__format_recursive if self.recursive else self.__format_simple - - # @staticmethod - # def __dummy_return_intact(template: str) -> str: - # return template - - def __escape_match(self, match: _re.Match[str]) -> str: - opening_brackets, inside_brackets, closing_brackets = match.groups() - if inside_brackets.strip() in self.format_dict: - n_opening = _n_brackets_after_escape_for_existing_key(len(opening_brackets)) - n_closing = _n_brackets_after_escape_for_existing_key(len(closing_brackets)) - return _rebuild_parsed_keyword(n_opening, inside_brackets, n_closing) + if format_dict: + self._format = self.__format_recursive if self.recursive else self.__format_single else: - return _rebuild_parsed_keyword(len(opening_brackets) * 2, inside_brackets, len(closing_brackets) * 2) + self._format = self.__dummy_return_intact @staticmethod - def __escape_match_no_keys(match: _re.Match[str]) -> str: - opening_brackets, inside_brackets, closing_brackets = match.groups() - return _rebuild_parsed_keyword(len(opening_brackets) * 2, inside_brackets, len(closing_brackets) * 2) + def __dummy_return_intact(template: str) -> str: + return template - def _escape_for_safe_format(self, template: str) -> str: - """ - Method to be called in safe mode - on template, before the actual format. - - Safe mode only replaces variables that exist in the dictionary. Leaves other curly brackets untouched. - """ - return _re_formatting_keyword_sub(self.__escape_match, template) - - def __format_simple(self, template: str) -> str: - if self.safe: - template = self._escape_for_safe_format(template) + def __format_single_unsafe(self, template: str): return template.format_map(self.format_dict) + def __format_single_safe_parts_gen(self, template: str) -> _t.Generator[str, None, None]: + """ + EAFP: https://docs.python.org/3/glossary.html#term-EAFP + + Instead of pre-escaping the whole template + (which would require basically re-implementing the entire format-parsing logic), + let's extract individual formatted pieces, actually try formatting them one by one, + and return anything that cannot be formatted as-is - without any processing at all. + + This generator returns such pieces - formatted or intact. + """ + format_dict = self.format_dict + to_piece_template = '{{{}}}'.format + + suffix: str = template + while suffix: + match = _re_formatting_keyword_match(suffix) + if not match: + break + + prefix = match.group('prefix') + open_brackets = match.group('open_brackets') + inside_brackets = match.group('inside_brackets') + closed_brackets = match.group('closed_brackets') + suffix = match.group('suffix') + + if prefix: + yield prefix + + piece_template = to_piece_template(inside_brackets) + # noinspection PyBroadException + try: + formatted_piece = piece_template.format_map(format_dict) + except Exception: + # If, for ANY reason, we're unable to format the piece, return the template piece intact: + yield open_brackets + yield inside_brackets + yield closed_brackets + continue + + # The key is found. Treat the piece as the actual formatting pattern. + # Formatting "eats" one set of brackets either way: + open_brackets = open_brackets[:-1] + closed_brackets = closed_brackets[:-1] + + # Now, even though we've succeeded, the template might've been pre-escaped. + if open_brackets and closed_brackets: + # The keyword is already pre-escaped. Return it intact: + formatted_piece = inside_brackets + + yield open_brackets + yield formatted_piece + yield closed_brackets + + if suffix: + yield suffix + + def __format_single_safe(self, template: str) -> str: + """ + Format the pattern (single iteration of formatting) in the safe mode. + Safe mode preserves any unknown ``{text patterns}`` inside curly brackets if they cannot be formatted. + Correctly handles any formatting patterns natively supported by python + (even the most fancy ones, involving ':', '!', attribute or index access, etc.). + + Useful when JSON/CSS-like code is in the formatted template. + """ + return ''.join(self.__format_single_safe_parts_gen(template)) + def __format_recursive(self, template: str) -> str: """ It's not actually recursive - because, you know, any recursion could be turned into iteration, @@ -119,20 +151,7 @@ class _Formatter: """ assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0 - # Cache before loop - to avoid even 'dot' operator - escape_func = self._escape_for_safe_format - format_dict: _t.Dict[str, _t.Any] = self.format_dict - - # The following two funcs defined here instead of method overrides - to simplify their local scope: - - def format_single_with_escape(_template: str): - _template = escape_func(_template) - return _template.format_map(format_dict) - - def format_single_no_escape(_template: str): - return _template.format_map(format_dict) - - format_single_func = format_single_with_escape if self.safe else format_single_no_escape + format_single_func = self.__format_single prev: str = '' new: str = template @@ -152,10 +171,6 @@ class _Formatter: # noinspection PyUnreachableCode return '' # just to be extra-safe, if RecursionError is treated as warning - @staticmethod - def _format(template: str) -> str: - raise NotImplementedError("This method should always be overridden after initialization.") - def __call__(self, template: str) -> str: if not isinstance(template, str): raise TypeError(f"Not a string: {template!r}") @@ -173,16 +188,17 @@ _input_types = _deepfreeze({ "Type the text template. " "To reference named substrings from format-dictionary, use this syntax: {substring_name}. For example:\n\n" "score_9, score_8_up, score_7_up, {char1_short}, standing next to {char2_short},\n" - "{char1_long}\n{char2_long}\n\n" - "Curly brackets that don't match dictionary keys will be handled according to the sanitization mode." + "{char1_long}\n{char2_long}" )}), 'recursive_format': (_IO.BOOLEAN, {'default': False, 'label_on': '❗ yes', 'label_off': 'no', 'tooltip': ( "Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks." )}), 'safe_format': (_IO.BOOLEAN, {'default': True, 'label_on': 'yes', 'label_off': 'no', 'tooltip': ( - "Safe mode: If a specific {keyword} doesn't exist in the dict, leave it as-is.\n" - "On: missing {keywords} preserved intact.\n" - "Off: missing {keywords} raise an error.\n\n" + "Safe mode: If a specific {text pattern} can't be formatted " + "(it doesn't exist in the dict or isn't a valid formatting pattern at all), " + "leave it as-is.\n" + "On: invalid {patterns} preserved intact.\n" + "Off: invalid {patterns} raise an error.\n\n" "Safe mode is recommended for templates with JSON, CSS, or other literal curly brackets." )}), 'show_status': (_IO.BOOLEAN, {'default': True, 'label_on': 'formatted string', 'label_off': 'no', 'tooltip': ( From 1289961c08c2c38cf6e89cfb3300271ece36be00 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 31 Aug 2025 05:04:11 +0500 Subject: [PATCH 13/17] + bugfix: properly handle multiline templates in safe-mode regex --- node_formatter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node_formatter.py b/node_formatter.py index 6ade2ef..51e3a86 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -29,7 +29,9 @@ _re_formatting_keyword_match = _re.compile( # Pre-compiled regex match to extra r'(?P\{+)' r'(?P[^{}]+)' r'(?P\}+)' - r'(?P[^{}].*)?$' + r'(?P[^{}].*)?$', + # flags=_re.DOTALL | _re.IGNORECASE, + flags = _re.DOTALL, # We need dot to match new lines, too ).match From dc230d279c6f9a0632513420f3b4d4f8a858d49c Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 31 Aug 2025 05:28:53 +0500 Subject: [PATCH 14/17] CHANGELOG --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 200d1fb..0c75016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ ## `TODO` The pack is considered feature-complete. -No other features planned. To do some fancy stuff with dictionaries, specialized node packs are recommended instead. +No other features planned (but I have nothing against improvements: feel free to [Pull Request](../../pulls)). + +To do some fancy stuff with dictionaries, specialized node packs are recommended instead. + +# v1.1.0 + +- `✨New feature` Safe-formatting mode: + - When an invalid pattern included in the formatted template - instead of throwing an error, leaves this part of the template as-is. Useful for templates with curly braces in them intended to stay: JSON/CSS. # v1.0.4 From 16ca2d70f0945a0c5c753d54ce12a99a4fd7de7a Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 31 Aug 2025 05:29:48 +0500 Subject: [PATCH 15/17] `v1.1.0` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0d6e1ad..bfdf52d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "string-constructor" description = "String formatting (compiling text from pieces) made for humans:\nβ€’ Turn parts of prompt into a dictionary - to share them all across the entire workflow as a single connection.\nβ€’ Compose your prompt in-place from these snippets and regular text - with just one text field.\nβ€’ Easily update the dictionary down the line - get different prompts from the same template.\nβ€’ Reference text chunks within each other to build complex hierarchies of self-updating prompts.\nβ€’ A true time-saver for regional prompting (aka area composition)." -version = "1.0.4" +version = "1.1.0" authors = [ {name = "Lex Darlog"} ] From 63846e246629b42636d7408ca44db0518f15bbd8 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 31 Aug 2025 05:34:05 +0500 Subject: [PATCH 16/17] + update `pyproject.toml` according to official recommendations --- pyproject.toml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bfdf52d..f741bed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,27 @@ [project] name = "string-constructor" -description = "String formatting (compiling text from pieces) made for humans:\nβ€’ Turn parts of prompt into a dictionary - to share them all across the entire workflow as a single connection.\nβ€’ Compose your prompt in-place from these snippets and regular text - with just one text field.\nβ€’ Easily update the dictionary down the line - get different prompts from the same template.\nβ€’ Reference text chunks within each other to build complex hierarchies of self-updating prompts.\nβ€’ A true time-saver for regional prompting (aka area composition)." version = "1.1.0" +description = "String formatting (compiling text from pieces) made for humans:\nβ€’ Turn parts of prompt into a dictionary - to share them all across the entire workflow as a single connection.\nβ€’ Compose your prompt in-place from these snippets and regular text - with just one text field.\nβ€’ Easily update the dictionary down the line - get different prompts from the same template.\nβ€’ Reference text chunks within each other to build complex hierarchies of self-updating prompts.\nβ€’ A true time-saver for regional prompting (aka area composition)." +license = {file = "LICENSE.md"} +readme = "README.md" authors = [ {name = "Lex Darlog"} ] -license = {file = "LICENSE.md"} -readme = "README.md" +requires-python = ">=3.7" dependencies = ["frozendict"] +classifiers = [ + # https://docs.comfy.org/registry/specifications#classifiers-recommended + "Operating System :: OS Independent", +] [project.urls] Repository = "https://github.com/Lex-DRL/ComfyUI-StringConstructor" +"Feedback/Bugs/Feature Requests" = "https://github.com/Lex-DRL/ComfyUI-StringConstructor/issues" # Used by Comfy Registry https://comfyregistry.org [tool.comfy] PublisherId = "lex-drl" DisplayName = "String Constructor (Text-Formatting)" -Icon = "" +# Icon = "https://raw.githubusercontent.com/username/repo/main/icon.png" +# Banner = "https://raw.githubusercontent.com/username/repo/main/banner.png" +# requires-comfyui = ">=0.3.51" # Node schema v3 released # TODO From b3b1a20e8ef51c5cec2eea59daffabd8591e3c87 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 31 Aug 2025 06:08:16 +0500 Subject: [PATCH 17/17] README: safe mode --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 776c76e..12e75c3 100644 --- a/README.md +++ b/README.md @@ -159,16 +159,33 @@ neg_common ### When you need curly braces themselves -To have the literal curly-brace characters inside the **formatted** prompt, you need to "escape" them: whenever you need one, you type it twice (`'{{'` or `'}}'`). Then, after formatting, it will turn to `'{'` or `'}'`, respectively. +#### Safe mode `✨New in v1.1.0` -Keep in mind though, that with [Recursive formatting](#recursive-formatting-), any `{{text}}` will become `{text}` after very first iteration, and thus on the next one, it still will be treated as a placeholder to put a string with a `text` key into. +The simplest solution - just enable `safe_format` toggle. It changes the node behavior: whenever formatter encounters any `{text inside curly braces}` that cannot be formatted _(for any reason: the dict misses this key, or it's an outright invalid pattern)_, it doesn't throw an error and instead simply leaves this part of template as-is. + +It's extremely useful for code-like templates (e.g., containing JSON or CSS). + +However, even for proper `{patterns}` referencing proper dict keys, the manual escaping (see below) also applies. + +> [!NOTE] +> Keep in mind that any spaces inside braces immediately make this pattern invalid. +> +> Thus, if you want to have something like: `{{my_key}}` _(so, `{my_key}` to be replaced + it's wrapped into another set of braces which you want to have in the output)_, the easiest way to do it without escaping is simply enabling safe mode and adding a couple of spaces inside the braces you want to stay. So: `{ {my_key} }` - the innermost part will be replaced with the value of `my_key` item from the dict, while the outer braces will stay. + +#### Manual escaping - double braces + +If you want to explicitly control which braces perform formatting and which don't - to have the literal curly-brace characters inside the prompt **after** formatting, you need to [escape](https://docs.python.org/3/library/string.html#format-string-syntax) them: whenever you need one, you type it twice (`'{{'` or `'}}'`). Then, after formatting, it will turn to `'{'` or `'}'`, respectively. + +Keep in mind though, that with escaping approach and [Recursive formatting](#recursive-formatting-), any `{{text}}` will become `{text}` after very first iteration, and thus on the next one, it still will be treated as a placeholder to put a value of the `text` key into. However, this might be exactly what you want for... ### Dynamic pattern aka conditional formatting In other words, you build a prompt, where **keys themselves** are compiled from pieces. For example: -- Your main text template has {{character_`{active_char}`}} pattern somewhere inside it. +- Your main text template has one of the following patterns somewhere inside it: + - {{character_`{active_char}`}} - if safe mode is off; + - {character_`{active_char}`} - if safe mode is on. - You also have a string named `active_char` in your dict, which you simply set to a number. - Also-also, you have strings named `character_1`, `character_2`, etc. - Then, with recursive formatting, depending on just a **single** value you set the `active_char` element to, the following happens: