Skip to content

Instantly share code, notes, and snippets.

@Hermann-SW
Last active September 11, 2026 09:21
Show Gist options
  • Select an option

  • Save Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5 to your computer and use it in GitHub Desktop.

Select an option

Save Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5 to your computer and use it in GitHub Desktop.
Latex labels for Graphviz (work in progress)
""" Latex labels for Graphviz """
import html
import re
import subprocess
from pylatexenc.latex2text import (
LatexNodes2Text,
MacroTextSpec,
get_default_latex_context_db as get_default_l2t_context_db,
)
from pylatexenc.latexwalker import (
LatexMacroNode,
LatexEnvironmentNode,
LatexWalker,
get_default_latex_context_db as get_default_walker_context_db,
)
from pylatexenc.macrospec import EnvironmentSpec, MacroSpec
BLACKBOARD_MAP = {"Z": "ℤ", "N": "ℕ", "C": "ℂ", "R": "ℝ", "Q": "ℚ"}
def custom_mathbb(node, l2tobj):
""" foobar """
arg_nodes = getattr(
node.nodeargd, "argnlist", getattr(node.nodeargd, "argnodelist", [])
)
arg = l2tobj.nodelist_to_text(arg_nodes)
return BLACKBOARD_MAP.get(arg, arg)
EXTRA_WALKER_MACROS = [
MacroSpec("textbf", "{"),
MacroSpec("cong"),
MacroSpec("mathbb", "{"),
MacroSpec("text", "{"),
MacroSpec("longrightarrow"),
MacroSpec("Longrightarrow"),
MacroSpec("iff"),
MacroSpec("implies"),
MacroSpec("sim"),
MacroSpec("approx"),
MacroSpec("equiv"),
MacroSpec("lcm"),
MacroSpec("lbr"),
MacroSpec("bar", "{"),
MacroSpec("vspace", "{"),
MacroSpec("ord"),
MacroSpec("gcd"),
MacroSpec("mod"),
]
walker_ctx = get_default_walker_context_db()
walker_ctx.add_context_category(
"math-extra", macros=EXTRA_WALKER_MACROS, prepend=True
)
walker_ctx.add_context_category(
"env-extra", environments=[EnvironmentSpec("cases")], prepend=True
)
l2t_ctx = get_default_l2t_context_db()
l2t_ctx.add_context_category(
"math-extra-override",
macros=[
MacroTextSpec("textbf", "@@@BOLDSTART@@@%(1)s@@@BOLDEND@@@"),
MacroTextSpec("cong", " ≅ "),
MacroTextSpec("mathbb", custom_mathbb),
MacroTextSpec("text", "%(1)s"),
MacroTextSpec("vert", "|"),
MacroTextSpec("longrightarrow", "⟶"),
MacroTextSpec("Longrightarrow", "⟹"),
MacroTextSpec("iff", "⇔ "),
MacroTextSpec("implies", "⇒"),
MacroTextSpec("sim", "~"),
MacroTextSpec("approx", "≈"),
MacroTextSpec("equiv", "≡"),
MacroTextSpec("Z", "ℤ"),
MacroTextSpec("N", "ℕ"),
MacroTextSpec("lcm", "lcm"),
MacroTextSpec("lbr", "@@@BRLEFT@@@"),
MacroTextSpec("bar", "@@@BARSTART@@@%(1)s@@@BAREND@@@"),
MacroTextSpec("vspace", "@@@VSPACESTART@@@%(1)s@@@VSPACEEND@@@"),
MacroTextSpec("ord", "ord"),
MacroTextSpec("gcd", "gcd"),
MacroTextSpec("mod", "mod"),
],
prepend=True,
)
def preprocess_sub_super_scripts(text):
"""Replaces superscripts/subscripts with safe placeholder tags free of underscores."""
text = re.sub(r"\^\{([^}]+)\}", r"@@@SUPSTART@@@\1@@@SUPEND@@@", text)
text = re.sub(
r"\^([a-zA-Z0-9\+\-]|\\iota)", r"@@@SUPSTART@@@\1@@@SUPEND@@@", text
)
text = re.sub(r"_\{([^}]+)\}", r"@@@SUBSTART@@@\1@@@SUBEND@@@", text)
text = re.sub(r"_([a-zA-Z0-9\+\-])", r"@@@SUBSTART@@@\1@@@SUBEND@@@", text)
return text
def postprocess_html_tags(text):
"""Converts safe tokens to valid Graphviz HTML labels."""
# Convert \vspace{N} to a small empty break tag
text = re.sub(
r"@@@VSPACESTART@@@(\d+)@@@VSPACEEND@@@",
r'<BR ALIGN="LEFT"/><FONT POINT-SIZE="\1">&nbsp;</FONT><BR ALIGN="LEFT"/>',
text
)
return (
text.replace("@@@BOLDSTART@@@", "<B>")
.replace("@@@BOLDEND@@@", "</B>")
.replace("@@@SUPSTART@@@", "<SUP>")
.replace("@@@SUPEND@@@", "</SUP>")
.replace("@@@SUBSTART@@@", "<SUB>")
.replace("@@@SUBEND@@@", "</SUB>")
.replace("@@@BRLEFT@@@", '<BR ALIGN="LEFT"/>')
.replace("@@@BARSTART@@@", "<O>")
.replace("@@@BAREND@@@", "</O>")
)
def find_unknown_macro(node, ctx):
""" foobar """
if node is None:
return None
if isinstance(node, LatexMacroNode):
if ctx.get_macro_spec(node.macroname) is None:
return node
if hasattr(node, "nodeargd") and node.nodeargd:
arg_list = getattr(
node.nodeargd,
"argnlist",
getattr(node.nodeargd, "argnodelist", []),
)
for child in arg_list:
err_node = find_unknown_macro(child, ctx)
if err_node:
return err_node
elif hasattr(node, "nodelist") and node.nodelist:
for child in node.nodelist:
err_node = find_unknown_macro(child, ctx)
if err_node:
return err_node
return None
# pylint: disable=too-many-locals
def process_cases_environment(env_node, converter, full_latex_str):
""" foobar """
rows = [[]]
for child in env_node.nodelist:
if isinstance(child, LatexMacroNode) and child.macroname == "\\":
rows.append([])
else:
rows[-1].append(child)
table_rows = []
num_rows = len([r for r in rows if r])
for row_nodes in rows:
if not row_nodes:
continue
first_pos = row_nodes[0].pos
last_node = row_nodes[-1]
last_pos = last_node.pos + last_node.len
row_str = full_latex_str[first_pos:last_pos]
if "&" in row_str:
expr_str, cond_str = row_str.split("&", 1)
else:
expr_str, cond_str = row_str, ""
w_expr = LatexWalker(expr_str, latex_context=walker_ctx)
n_expr, _, _ = w_expr.get_latex_nodes()
expr = converter.nodelist_to_text(n_expr).strip()
w_cond = LatexWalker(cond_str, latex_context=walker_ctx)
n_cond, _, _ = w_cond.get_latex_nodes()
cond = converter.nodelist_to_text(n_cond).strip()
expr = postprocess_html_tags(html.escape(expr))
cond = postprocess_html_tags(html.escape(cond))
if len(table_rows) == 0:
table_rows.append(
f"<TR>"
f'<TD ROWSPAN="{num_rows}" VALIGN="MIDDLE" ALIGN="RIGHT" BORDER="0">'
f'<FONT POINT-SIZE="22">&#123;</FONT></TD>'
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">{expr}</TD>'
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">&nbsp;&nbsp;&nbsp;&nbsp;{cond}</TD>'
f"</TR>"
)
else:
table_rows.append(
f"<TR>"
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">{expr}</TD>'
f'<TD ALIGN="LEFT" VALIGN="MIDDLE" BORDER="0">&nbsp;&nbsp;&nbsp;&nbsp;{cond}</TD>'
f"</TR>"
)
return (
'<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="1">'
+ "".join(table_rows)
+ "</TABLE>"
)
def latex_to_graphviz_html(latex_str):
""" foobar """
latex_str = preprocess_sub_super_scripts(latex_str)
walker = LatexWalker(latex_str, latex_context=walker_ctx)
nodes, _, _ = walker.get_latex_nodes()
for node in nodes:
missing_node = find_unknown_macro(node, l2t_ctx)
if missing_node:
raise ValueError(f"Unknown macro \\{missing_node.macroname}")
converter = LatexNodes2Text(latex_context=l2t_ctx)
if r"\begin{cases}" in latex_str:
for node in nodes:
if (
isinstance(node, LatexEnvironmentNode)
and node.environmentname == "cases"
):
cases_table = process_cases_environment(
node, converter, latex_str
)
prefix_str = latex_str[: node.pos]
suffix_str = latex_str[node.pos + node.len :]
if r"\\" in prefix_str:
lines = prefix_str.split(r"\\")
line1_html = latex_to_graphviz_html(lines[0])[1:-1]
line2_html = latex_to_graphviz_html(lines[1])[1:-1]
suffix_html = latex_to_graphviz_html(suffix_str)[1:-1]
full_html = (
'<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0">'
f'<TR><TD BORDER="0" ALIGN="LEFT" COLSPAN="2">{line1_html}</TD></TR>'
f'<TR><TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT">{line2_html}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT">{cases_table}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE" ALIGN="LEFT">{suffix_html}</TD></TR>'
"</TABLE>"
)
return f"<{full_html}>"
prefix_html = latex_to_graphviz_html(prefix_str)[1:-1]
suffix_html = latex_to_graphviz_html(suffix_str)[1:-1]
full_html = (
'<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0"><TR>'
f'<TD BORDER="0" VALIGN="MIDDLE">{prefix_html}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE">{cases_table}</TD>'
f'<TD BORDER="0" VALIGN="MIDDLE">{suffix_html}</TD>'
"</TR></TABLE>"
)
return f"<{full_html}>"
plain_text = converter.nodelist_to_text(nodes)
escaped_text = html.escape(plain_text)
# Convert placeholders to Graphviz HTML tags for standard nodes
html_label_body = postprocess_html_tags(escaped_text.replace("\n", "<BR/>"))
return f"<{html_label_body}>"
def_label = latex_to_graphviz_html(
r"""\textbf{Def 2.1 }Number-theoretic function:\\"""
r"""a: \mathbb{N}\to \mathbb{C}"""
)
ex_label = latex_to_graphviz_html(
r"""\textbf{Ex 2.2a }for s\in\mathbb{C}: \iota^{s}(n) := n^s \forall n \in
\mathbb{N} is s-th power\\function (s=0 constant one function, identity \iota^1 =: \iota)"""
)
def_3_13_label = latex_to_graphviz_html(
r"""\textbf{Def 3.13 } Ring R, x\in R: if \exists y\in R\setminus \{0\}: xy=0,
then x is called zero divisor"""
)
ex_3_14_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.14 } zero divisors in \Z/3\Z: \bar{0}, so has no zero divisors\vspace{3}"""
r"""zero divisors in \Z/4\Z: \bar{0},\bar{2}, so has zero divisor\\"""
)
def_3_15_label = latex_to_graphviz_html(
r"""\textbf{Def 3.15 } Ring R, x\in R: if \exists y\in R: xy=1,\\then x is called a unit"""
)
ex_3_16_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.16 } units in \Z/3\Z: \bar{1},\bar{2}\vspace{3}"""
r"""units in \Z/4\Z: \bar{1},\bar{3}"""
)
prop_3_17_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.17 } R ring\lbr"""
r"""(a) R^{\times}:=\{x\in R: x is unit\} with \cdot\ is (abelian) group of units\lbr"""
r"""(b) x\in R^{\times} \implies\ x no zero divisor\lbr"""
r"""(c) R finite, then also: x\in R no zero divisor \implies\ x is unit\lbr"""
)
ex_3_18_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.18 } Finite ring R=\Z/n\Z, n\in \N, x\in R:\vspace{5}"""
r"""\bar{x} unit \iff \bar{x} no zero divisor\lbr """
r"""Units in R are called coprime residue classes modulo n\lbr """
r"""(\Z/n\Z)^{\times} is called multiplicative group of integers modulo n"""
)
def_3_19_label = latex_to_graphviz_html(
r"""\textbf{Def 3.19 } (Commutative) ring R
with R^{\times} = R \setminus \{0\} is called field"""
)
ex_3_20_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.20 } \Z/3\Z\ is field, \Z/4\Z\ and zero ring are no fields."""
)
thm_3_21_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.21 } n\in \N, these are equivalent\lbr"""
r"""(i) n is prime\lbr"""
r"""(ii) \Z/n\Z\ is field\lbr"""
r"""(iii) \Z/n\Z\ has no zero divisors\lbr"""
)
prop_3_22_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.22 } a\in \mathbb{N}, \bar{a}\in \Z/n\Z:\vspace{3}"""
r"""\bar{a}\in (\Z/n\Z)^{\times} \iff \gcd(a,n)=1"""
)
cor_3_23_label = latex_to_graphviz_html(
r"""\textbf{Cor 3.23 } n\in \mathbb{N}, a\in \Z:\vspace{3}"""
r"""(\exists x\in \Z: ax\equiv 1 (\mod\ n)) \iff \gcd(a,n)=1"""
)
ex_3_24_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.24 } Solutions for\\"""
r"""3x\equiv 1 (\mod\ 37): L=25+37\Z"""
)
prop_3_25_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.25 } a,b\in \Z, n\in\N\\"""
r"""(\exists x\in\Z: ax\equiv b (\mod\ n)) \iff \gcd(a,n)\vert b"""
)
ex_3_26_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.26 } (a) \nexists x\in \Z: 15x\equiv 7 (\mod\ 21)\lbr"""
r"""(b) \exists x\in\Z: 15x\equiv 6 (\mod\ 21)\lbr"""
)
def_3_27_label = latex_to_graphviz_html(
r"""\textbf{Def 3.27 } Order \vert G\vert\ of group\\"""
r"""is number of its elements"""
)
ex_3_28_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.28 }\vert(\Z/n\Z)^{\times}\vert=\phi(n)"""
)
prop_3_29_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.29 } G finite abelian group.\lbr
\forall g\in G: g^{\vert G\vert} =1 """
)
thm_3_30_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.30 } (Fermat-Euler theorem) \vspace{5}"""
r"""n\in \mathbb{N}. """
r"""\forall \bar{a}\in (\Z/n\Z)^{\times}:\ \bar{a}^{\phi(n)} =\bar{1} """
)
ex_3_31_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.31 } 3^{19}\equiv 10 (\mod\ 17)"""
)
cor_3_32_label = latex_to_graphviz_html(
r"""\textbf{Cor 3.32 } (Fermat's little theorem) p prime:\vspace{3}"""
r"""(a) \forall \bar{a}\in \mathbb{F}_p^{\times}: \bar{a}^{p-1}=\bar{1}\vspace{5}"""
r"""(b) \forall \bar{a}\in \mathbb{F}_p: \bar{a}^p=\bar{a}\lbr"""
)
def_3_33_label = latex_to_graphviz_html(
r"""\textbf{Def 3.33 } Group G is cyclic:\lbr
\exists g\in G generator with\lbr
G = \{g^n: n\in \Z\}=:<g>\lbr"""
)
rem_3_34_label = latex_to_graphviz_html(
r"""\textbf{Rem 3.34 } Every cyclic group is abelian."""
)
ex_3_35_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.35 }(a) group (\Z/5\Z)^{\times} = \{\bar{1},\bar{2},"""
r"""\bar{3},\bar{4}\} is cyclic\vspace{3}"""
r"""(b) group (\Z/8\Z)^{\times} = \{\bar{1},\bar{3},\bar{5},\bar{7}\} is not cyclic\lbr"""
r"""(c) additive group \Z\ is cyclic\vspace{3}"""
r"""(d) additive group \Z/m\Z =\{\bar{0},\bar{1},\dots,\bar{m-1}\}\ is cyclic\lbr"""
)
def_3_36_label = latex_to_graphviz_html(
r"""\textbf{Def 3.36 } G finite abelian group.\lbr
Order \ord(G):=\min\{n\in \mathbb{N}: g^n=1\}\lbr """
)
ex_3_37_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.37 }For G=(\Z/5\Z)^{\times}:\vspace{3}"""
r"""(a) ord(\bar{2})=4\vspace{3}"""
r"""(b) ord(\bar{4})=2\lbr"""
)
prop_3_38_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.38 }G finite abelian group:
G is cyclic \iff \exists g\in G with ord(g)=\vert G\vert"""
)
prop_3_39_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.39 }G finite abelian group,\\g\in G, m\in \Z: ord(g)\vert m \iff g^m=1"""
)
cor_3_40_label = latex_to_graphviz_html(
r"""\textbf{Cor 3.40 } G finite abelian group.\lbr
\forall g\in G: ord(g) \vert\ \vert G\vert """
)
def_3_41_label = latex_to_graphviz_html(
r"""\textbf{Def 3.41 } G finite abelian group.\lbr
Exponent \exp(G):=\min\{n\in \mathbb{N}: g^n=1 \forall g\in G\}\lbr """
)
ex_3_42_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.42 }(a) exp((\Z/5\Z)^{\times})=4\lbr
(b) exp((\Z/8\Z)^{\times})=2\lbr"""
)
prop_3_43_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.43 }G finite abelian group G:\lbr """
r"""(a) \exp(G) \vert\ \vert G\vert\ \lbr """
r"""(b) \exp(G)= \lcm(\{ord(g): g\in G\})\lbr"""
)
def_3_44_label = latex_to_graphviz_html(
r"""\textbf{Def 3.44 } homomorphism\\ isomorphism, \cong """
)
ex_3_45_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.45 }K field implies\lbr
det:GL_n(K)\to K^{\times} homomorphism"""
)
prop_3_46_label = latex_to_graphviz_html(
r"""\textbf{Prop 3.46 }G, H groups, \psi: G\to\ H homomorphism:\lbr """
r"""(a) \psi \text{ injective} \iff \text{ker}(\psi):=\{g\in G: \psi(g)=1\}=\{1\}\lbr """
r"""(b) \psi \text{ isomorphism} \text{implies} \psi^{-1}: H\to G isomorphism\lbr"""
)
thm_3_47_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.47 }For cyclic group G:\\ G \cong \begin{cases}
\mathbb{Z} & \text{for }G\text{ infinite,}\\
\Z /|G|\mathbb{Z} & \text{for }G\text{ finite.} \end{cases}"""
)
ex_3_48_label = latex_to_graphviz_html(
r"""\textbf{Ex 3.48 } (\Z/5\Z)^{\times} \cong \Z/4\Z\\ \psi: \begin{cases}
\Z/4\Z &\to\ (\Z/5\Z)^{\times}\\
a &\mapsto\ \bar{2}^a \end{cases}\lbr"""
)
thm_3_49_label = latex_to_graphviz_html(
r"""\textbf{Thm 3.49 }G finite abelian group:
G is cyclic \iff \exp(G) = \vert G\vert"""
)
dot_content = f"""digraph G {{
layout=neato;
overlap="false";
sep="+15";
node [shape=box, fontsize=12, margin="0.15,0.1"];
labelloc=t
label="3.3 prime residue classes and Fermat-Euler theorem (Def 3.13 — Cor 3.32)\n3.4 cyclic groups (Def 3.33 — Thm 3.49)\n\n "
# def_2_1 [label={def_label}];
# ex_2_2a [label={ex_label}];
def_3_13 [label={def_3_13_label}];
ex_3_14 [label={ex_3_14_label}];
def_3_15 [label={def_3_15_label}];
ex_3_16 [label={ex_3_16_label}];
prop_3_17 [label={prop_3_17_label}];
ex_3_18 [label={ex_3_18_label}];
def_3_19 [label={def_3_19_label}];
ex_3_20 [label={ex_3_20_label}];
thm_3_21 [label={thm_3_21_label}];
prop_3_22 [label={prop_3_22_label}];
cor_3_23 [label={cor_3_23_label}];
ex_3_24 [label={ex_3_24_label}];
prop_3_25 [label={prop_3_25_label}];
ex_3_26 [label={ex_3_26_label}];
def_3_27 [label={def_3_27_label}];
ex_3_28 [label={ex_3_28_label}];
prop_3_29 [label={prop_3_29_label}];
thm_3_30 [label={thm_3_30_label}];
ex_3_31 [label={ex_3_31_label}];
cor_3_32 [label={cor_3_32_label}];
def_3_33 [label={def_3_33_label}];
rem_3_34 [label={rem_3_34_label}];
ex_3_35 [label={ex_3_35_label}];
def_3_36 [label={def_3_36_label}];
ex_3_37 [label={ex_3_37_label}];
prop_3_38 [label={prop_3_38_label}];
prop_3_39 [label={prop_3_39_label}];
cor_3_40 [label={cor_3_40_label}];
def_3_41 [label={def_3_41_label}];
ex_3_42 [label={ex_3_42_label}];
prop_3_43 [label={prop_3_43_label}];
def_3_44 [label={def_3_44_label}];
ex_3_45 [label={ex_3_45_label}];
prop_3_46 [label={prop_3_46_label}];
thm_3_47 [label={thm_3_47_label}];
ex_3_48 [label={ex_3_48_label}];
thm_3_49 [label={thm_3_49_label}];
def_3_13 -> ex_3_14 [style=dotted];
def_3_15 -> ex_3_16 [style=dotted];
{{ def_3_13 def_3_15 }} -> prop_3_17 [style=dotted];
ex_3_14 -> ex_3_16 [style=invis,arrowhead=none];
prop_3_17 -> ex_3_18 [style=dotted];
prop_3_17 -> thm_3_21;
{{ ex_3_16 def_3_19 }} -> ex_3_20 [style=dotted];
ex_3_20 -> thm_3_21;
prop_3_29 -> def_3_36;
thm_3_21 -> prop_3_22 [style=dotted];
prop_3_22 -> cor_3_23 [style=dotted];
def_3_19 -> thm_3_21;
cor_3_23 -> ex_3_24 [style=dotted];
cor_3_23 -> prop_3_25 [style=dotted];
prop_3_25 -> ex_3_26 [style=dotted];
ex_3_24 -> def_3_27 [style=invis,arrowhead=none];
prop_3_22 -> ex_3_28;
def_3_27 -> ex_3_28 [style=dotted];
def_3_33 -> ex_3_35 [style=dotted];
def_3_36 -> ex_3_37 [style=dotted];
thm_3_30 -> {{ ex_3_31 cor_3_32 }};
{{ ex_3_28 prop_3_29 }} -> thm_3_30;
ex_3_31 -> cor_3_32 [style=invis,arrowhead=none];
def_3_33 -> rem_3_34 [style=dotted label="https://gist.github.com/Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5 "
URL="https://gist.github.com/Hermann-SW/12c7644ac0c75b4eb019f76c3f023fe5"]
def_3_41 -> prop_3_43 [style=dotted];
prop_3_39 -> cor_3_40;
prop_3_39 -> prop_3_43;
def_3_44 -> {{ ex_3_45 prop_3_46 }} [style=dotted];
prop_3_38 -> thm_3_47;
prop_3_39 -> thm_3_47;
def_3_36 -> prop_3_38 [style=dotted];
prop_3_46 -> thm_3_47 [style=dotted];
prop_3_29 -> cor_3_40;
def_3_41 -> ex_3_42 [style=dotted];
{{ ex_3_35 thm_3_47 }} -> ex_3_48;
{{ prop_3_39 prop_3_43 }} -> thm_3_49;
prop_3_38 -> thm_3_49;
}}"""
DOTFILENAME = "graph_unicode.dot"
with open(DOTFILENAME, "w", encoding="utf-8") as f:
f.write(dot_content)
print("Generated .dot file:\n")
print(dot_content)
subprocess.run(
["dot", "-Tpdf", DOTFILENAME, "-o", "graph_output.pdf"], check=True
)
@Hermann-SW

Hermann-SW commented Sep 11, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment