Skip to content

Instantly share code, notes, and snippets.

@odudex
Last active January 15, 2025 18:06
Show Gist options
  • Save odudex/13d29838764d411217f7a183a14f5f1c to your computer and use it in GitHub Desktop.
Save odudex/13d29838764d411217f7a183a14f5f1c to your computer and use it in GitHub Desktop.
class Node:
"""
A simple tree node that only stores:
- text: The 'prefix' of the expression before an open parenthesis,
or the entire expression if no parentheses exist.
- children: Any sub-expressions contained within parentheses (split by commas at top level).
- level: An integer used to control indentation depth.
"""
def __init__(self, text, children=None, level=0):
self.text = text
self.children = children if children is not None else []
self.level = level
def split_top_level_args(s, delimiter=","):
"""
Splits string s by the top-level delimiter (by default a comma).
"""
parts = []
bracket_level = 0
current = []
for ch in s:
if ch == "(":
bracket_level += 1
current.append(ch)
elif ch == ")":
bracket_level -= 1
current.append(ch)
elif ch == delimiter and bracket_level == 0:
parts.append("".join(current).strip())
current = []
else:
current.append(ch)
if current:
parts.append("".join(current).strip())
return parts
def parse_expression(expr, level=0):
"""
Recursively parse the expression into a Node that reflects parentheses structure.
Steps:
- If there's no parenthesis at all, this is a leaf node.
- Otherwise:
prefix: the text before '('
inside: the substring inside the outermost parentheses
children: sub-expressions inside 'inside', separated at top-level commas
"""
expr = expr.strip()
pos = expr.find("(")
if pos == -1:
# No '(' => leaf node
return Node(text=expr, children=[], level=level)
prefix = expr[:pos].strip()
inside = expr[pos + 1 : -1].strip()
sub_expressions = split_top_level_args(inside)
children = [
parse_expression(sub_expr, level=level + 1) for sub_expr in sub_expressions
]
return Node(text=prefix, children=children, level=level)
def join_closing_parens(lines):
"""
Post-processing step:
If a line is only one or more closing parentheses (possibly with indentation),
append them to the end of the previous line, so no line is just ')'.
"""
i = len(lines) - 1
while i > 0:
stripped = lines[i].strip()
# Check if stripped is only some number of ')'
if stripped and all(ch == ")" for ch in stripped):
# Append them (minus indentation) to previous line
lines[i - 1] = "{0}{1}".format(lines[i - 1], stripped)
lines.pop(i)
i -= 1
return lines
def node_to_indented_string(node, max_line_width=25):
"""
Convert the Node tree into an indented list of strings with two rules:
1) If any child is a leaf, flatten all children into a single line with the parent.
2) Remove lines that only contain ')', by merging them into the previous line.
3) Ensure no line exceeds max_line_width by breaking long lines.
"""
# If no children => leaf node
if not node.children:
indent = " " * node.level
return ["{0}{1}".format(indent, node.text)]
# If any child is a leaf, flatten them all
# It's likely it will group multi and thresh children together
any_child_is_leaf = any(not c.children for c in node.children)
if any_child_is_leaf and node.level > 0:
# Flatten children on one line
indent = " " * node.level
line = "{0}{1}(".format(indent, node.text)
child_texts = []
for child in node.children:
child_str = node_to_indented_string(child, max_line_width)
# # compress any newlines to single space
# child_str = child_str.replace("\n", " ")
child_texts.append(child_str[0].strip())
line += ",".join(child_texts) + ")"
# If flattened line is short enough, return it
if len(line) <= max_line_width:
return [line]
# Else break it into multiple lines as if here was no leaf children
# Multi-line approach
indent = " " * node.level
lines = []
lines.append("{0}{1}(".format(indent, node.text))
for i, child in enumerate(node.children):
child_lines = node_to_indented_string(child, max_line_width)
# If not the last child, add a trailing comma
if i < len(node.children) - 1:
child_lines[-1] = "{0},".format(child_lines[-1])
lines.extend(child_lines)
lines.append("{0})".format(indent))
# Post-process lines to join any lines that are just ')'
lines = join_closing_parens(lines)
return lines
def break_lines(line, max_line_width):
"""
Break a line into multiple lines if it exceeds max_line_width.
"""
if len(line) <= max_line_width:
return [line]
indent = 0
while True:
if line[indent] != " ":
break
indent += 1
data = line[indent:]
parts = []
while len(data) > max_line_width - indent:
parts.append(" " * indent + data[: max_line_width - indent])
data = data[max_line_width - indent :]
parts.append(" " * indent + data)
return parts
if __name__ == "__main__":
import sys
max_width = 25
try:
max_width = int(sys.argv[1])
except:
pass
miniscripts = [
# Sipa's example scripts
"pk(A)",
"or_b(pk(A),s:pk(B))",
"or_d(pk(A),pkh(B))",
"and_v(v:pk(A),or_d(pk(B),older(12960)))",
"thresh(3,pk(A),pk(B),pk(C),older(12960))",
"andor(pk(A),older(1008),pk(B))",
"t:or_c(pk(A),and_v(v:pk(B),or_c(pk(C),v:hash160(e7d285b4817f83f724cd29394da75dfc84fe639e))))",
"andor(pk(A),or_i(and_v(v:pkh(B),hash160(e7d285b4817f83f724cd29394da75dfc84fe639e)),older(1008)),pk(C))",
# Other examples
"or_d(pk(A),and_v(v:pkh(B),older(6)))",
"and_v(or_c(pk(B),or_c(pk(C),v:older(1000))),pk(A))",
"or_d(multi(2,A,B),and_v(v:thresh(2,pkh(C),a:pkh(D),a:pkh(E)),older(144)))",
"andor(multi(2,A,B,C),or_i(and_v(v:pkh(D),after(230436)),thresh(2,pk(E),s:pk(F),s:pk(G),snl:after(230220))),and_v(v:thresh(2,pkh(H),a:pkh(I),a:pkh(J)),after(230775)))",
"andor(multi(2,A,B,C),or_i(and_v(v:pkh(D),after(1737233087)),thresh(2,pk(E),s:pk(F),s:pk(G),snl:after(1737146691))),and_v(v:thresh(2,pkh(H),a:pkh(I),a:pkh(J)),after(1737319495)))",
"tr(A, and_v(v:pk(B,older(65535)))"
]
for miniscript in miniscripts:
# 1) Parse into a Node tree
tree = parse_expression(miniscript)
# 2) Convert to an indented string with the new rules
indented_lines = node_to_indented_string(tree, max_width)
final_lines = []
for line in indented_lines:
final_lines.extend(break_lines(line, max_width))
print("\n".join(final_lines))
print("\n")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment