Last active
January 15, 2025 18:06
-
-
Save odudex/13d29838764d411217f7a183a14f5f1c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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