Skip to content

Commit 27f52c6

Browse files
doudenLjon4ik4
andauthored
feat: Added possibility for shared numbering of theorems. (#164)
* feat: Added possibility for shared numbering of theorems, fixing issue #64 (#161) Added optioin to have directives share numbering. Reflected also in docs. * Add config validation Introduces a check_config_values function to validate Sphinx configuration values at runtime, ensuring correct types and defaults for proof-related settings. --------- Co-authored-by: Leonid Ryvkin <ryvkin@gmx.com>
1 parent 8fdfb3d commit 27f52c6

6 files changed

Lines changed: 183 additions & 33 deletions

File tree

docs/source/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,5 @@
5656
# MyST Parser Configuration
5757

5858
myst_enable_extensions = ["dollarmath", "amsmath"]
59+
60+
prf_realtyp_to_countertyp = {}

docs/source/options.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,61 @@ sphinx:
3131
3232
Add `proof_minimal_theme = True` to your `conf.py`
3333

34+
## Shared numbering
35+
36+
By default, each type of (prf-)directive has their own numbering and counter. This can be changed by setting the option `prf_realtyp_to_countertyp` to a dictionary associating to a directive which the counter of which directive it should use.
37+
38+
### Sphinx Project
39+
40+
In `conf.py`, e.g. to have a shared counter for all directives:
41+
42+
```
43+
prf_realtyp_to_countertyp = {
44+
"axiom": "theorem",
45+
"theorem": "theorem",
46+
"lemma": "theorem",
47+
"algorithm": "theorem",
48+
"definition": "theorem",
49+
"remark": "theorem",
50+
"conjecture": "theorem",
51+
"corollary": "theorem",
52+
"criterion": "theorem",
53+
"example": "theorem",
54+
"property": "theorem",
55+
"observation": "theorem",
56+
"proposition": "theorem",
57+
"assumption": "theorem",
58+
"notation": "theorem",
59+
}
60+
```
61+
62+
In the following case, the directives `lemma`, `conjecture`, `corollary` and `proposition` will share the counter with `theorem`, while `axiom` and `assumption` will share the counter with `definition`. All other directives would use their original counter.
63+
64+
65+
```
66+
prf_realtyp_to_countertyp = {
67+
"lemma": "theorem",
68+
"conjecture": "theorem",
69+
"corollary": "theorem",
70+
"proposition": "theorem",
71+
"axiom": "definition",
72+
"assumption": "definition",
73+
}
74+
```
75+
76+
````{warning}
77+
The association of a counter to a directive is not transitive: Let us consider the following configuration:
78+
79+
```
80+
prf_realtyp_to_countertyp = {
81+
"lemma": "theorem",
82+
"conjecture": "lemma",
83+
}
84+
```
85+
86+
The `lemma` and `theorem` directives share a counter, however the `conjecture` directive has a separate counter (the `lemma` counter which is **not** used by `lemma` directives).
87+
````
88+
3489
## Title format
3590
3691
By default, the directive titles are formatted as `Name x.y.z (Title)`, where `Name` is the name of the directive (e.g., Proof, Theorem, Definition), `x.y.z` is the numbering of the directive, and `Title` is the optional title provided by the user.

sphinx_proof/__init__.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,13 @@ def copy_asset_files(app: Sphinx, exc: Union[bool, Exception]):
102102
def setup(app: Sphinx) -> Dict[str, Any]:
103103

104104
app.add_config_value("proof_minimal_theme", False, "html")
105+
app.add_config_value("prf_realtyp_to_countertyp", {}, "html")
105106
app.add_config_value("proof_title_format", " (%t)", "html")
106-
app.add_config_value("proof_number_weight", "", "env")
107-
app.add_config_value("proof_title_weight", "", "env")
107+
app.add_config_value("proof_number_weight", "", "html")
108+
app.add_config_value("proof_title_weight", "", "html")
108109

109110
app.add_css_file("proof.css")
111+
app.connect("config-inited", check_config_values)
110112
app.connect("build-finished", copy_asset_files)
111113
app.connect("config-inited", init_numfig)
112114
app.connect("env-purge-doc", purge_proofs)
@@ -142,3 +144,64 @@ def setup(app: Sphinx) -> Dict[str, Any]:
142144
"parallel_read_safe": True,
143145
"parallel_write_safe": True,
144146
}
147+
148+
149+
def check_config_values(app: Sphinx, config: Config) -> None:
150+
"""Check configuration values."""
151+
# Check if proof_minimal_theme is boolean
152+
if not isinstance(config.proof_minimal_theme, bool):
153+
logger.warning(
154+
"'proof_minimal_theme' config value must be a boolean. "
155+
"Using default value False."
156+
)
157+
config.proof_minimal_theme = False
158+
159+
# Check of prf_realtyp_to_countertyp is a dictionary
160+
if not isinstance(config.prf_realtyp_to_countertyp, dict):
161+
logger.warning(
162+
"'prf_realtyp_to_countertyp' config value must be a dictionary. "
163+
"Using default empty dictionary."
164+
)
165+
config.prf_realtyp_to_countertyp = {}
166+
# Check if each key and each value in prf_realtyp_to_countertyp
167+
# is a valid proof type
168+
for key, value in config.prf_realtyp_to_countertyp.items():
169+
if key not in PROOF_TYPES:
170+
logger.warning(
171+
f"Key '{key}' in 'prf_realtyp_to_countertyp' is not "
172+
"a valid proof type. "
173+
"It will be removed."
174+
)
175+
del config.prf_realtyp_to_countertyp[key]
176+
elif value not in PROOF_TYPES:
177+
logger.warning(
178+
f"Value '{value}' in 'prf_realtyp_to_countertyp' is not "
179+
"a valid proof type. It will be removed."
180+
)
181+
del config.prf_realtyp_to_countertyp[key]
182+
# Check if proof_title_format is a string
183+
if not isinstance(config.proof_title_format, str):
184+
logger.warning(
185+
"'proof_title_format' config value must be a string."
186+
"Using default value ' (%t)'."
187+
)
188+
config.proof_title_format = " (%t)"
189+
elif "%t" not in config.proof_title_format:
190+
logger.warning(
191+
"'proof_title_format' config value must contain the "
192+
"substring '%t' to print a title."
193+
)
194+
# Check if proof_number_weight is a string
195+
if not isinstance(config.proof_number_weight, str):
196+
logger.warning(
197+
"'proof_number_weight' config value must be a string. "
198+
"Using default value ''."
199+
)
200+
config.proof_number_weight = ""
201+
# Check if proof_title_weight is a string
202+
if not isinstance(config.proof_title_weight, str):
203+
logger.warning(
204+
"'proof_title_weight' config value must be a string. "
205+
"Using default value ''."
206+
)
207+
config.proof_title_weight = ""

sphinx_proof/directive.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@
2020
logger = logging.getLogger(__name__)
2121

2222

23+
DEFAULT_REALTYP_TO_COUNTERTYP = {
24+
"axiom": "axiom",
25+
"theorem": "theorem",
26+
"lemma": "lemma",
27+
"algorithm": "algorithm",
28+
"definition": "definition",
29+
"remark": "remark",
30+
"conjecture": "conjecture",
31+
"corollary": "corollary",
32+
"criterion": "criterion",
33+
"example": "example",
34+
"property": "property",
35+
"observation": "observation",
36+
"proposition": "proposition",
37+
"assumption": "assumption",
38+
"notation": "notation",
39+
}
40+
41+
2342
class ElementDirective(SphinxDirective):
2443
"""A custom Sphinx Directive"""
2544

@@ -36,13 +55,16 @@ class ElementDirective(SphinxDirective):
3655

3756
def run(self) -> List[Node]:
3857
env = self.env
39-
typ = self.name.split(":")[1]
58+
realtyp = self.name.split(":")[1]
59+
countertyp = env.config.prf_realtyp_to_countertyp.get(
60+
realtyp, DEFAULT_REALTYP_TO_COUNTERTYP[realtyp]
61+
)
4062
serial_no = env.new_serialno()
4163
if not hasattr(env, "proof_list"):
4264
env.proof_list = {}
4365

4466
# If class in options add to class array
45-
classes, class_name = ["proof", typ], self.options.get("class", [])
67+
classes, class_name = ["proof", realtyp], self.options.get("class", [])
4668
if class_name:
4769
classes.extend(class_name)
4870

@@ -53,15 +75,15 @@ def run(self) -> List[Node]:
5375
node_id = f"{label}"
5476
else:
5577
self.options["noindex"] = True
56-
label = f"{typ}-{serial_no}"
57-
node_id = f"{typ}-{serial_no}"
78+
label = f"{realtyp}-{serial_no}"
79+
node_id = f"{realtyp}-{serial_no}"
5880
ids = [node_id]
5981

6082
# Duplicate label warning
6183
if not label == "" and label in env.proof_list.keys():
6284
path = env.doc2path(env.docname)[:-3]
6385
other_path = env.doc2path(env.proof_list[label]["docname"])
64-
msg = f"duplicate {typ} label '{label}', other instance in {other_path}"
86+
msg = f"duplicate {realtyp} label '{label}', other instance in {other_path}"
6587
logger.warning(msg, location=path, color="red")
6688

6789
title_text = ""
@@ -72,13 +94,13 @@ def run(self) -> List[Node]:
7294

7395
textnodes, messages = self.state.inline_text(title_text, self.lineno)
7496

75-
section = nodes.section(classes=[f"{typ}-content"], ids=["proof-content"])
97+
section = nodes.section(classes=[f"{realtyp}-content"], ids=["proof-content"])
7698
self.state.nested_parse(self.content, self.content_offset, section)
7799

78100
if "nonumber" in self.options:
79101
node = unenumerable_node()
80102
else:
81-
node_type = NODE_TYPES[typ]
103+
node_type = NODE_TYPES[countertyp]
82104
node = node_type()
83105

84106
node.document = self.state.document
@@ -90,17 +112,18 @@ def run(self) -> List[Node]:
90112
node["classes"].extend(classes)
91113
node["title"] = title_text
92114
node["label"] = label
93-
node["type"] = typ
115+
node["countertype"] = countertyp
116+
node["realtype"] = realtyp
94117

95118
env.proof_list[label] = {
96119
"docname": env.docname,
97-
"type": typ,
120+
"countertype": countertyp,
121+
"realtype": realtyp,
98122
"ids": ids,
99123
"label": label,
100124
"prio": 0,
101125
"nonumber": True if "nonumber" in self.options else False,
102126
}
103-
104127
return [node]
105128

106129

@@ -117,16 +140,16 @@ class ProofDirective(SphinxDirective):
117140
}
118141

119142
def run(self) -> List[Node]:
120-
typ = self.name.split(":")[1]
143+
realtyp = self.name.split(":")[1]
121144

122145
# If class in options add to class array
123-
classes, class_name = ["proof", typ], self.options.get("class", [])
146+
classes, class_name = ["proof", realtyp], self.options.get("class", [])
124147
if class_name:
125148
classes.extend(class_name)
126149

127-
section = nodes.admonition(classes=classes, ids=[typ])
150+
section = nodes.admonition(classes=classes, ids=[realtyp])
128151

129-
self.content[0] = "{}. ".format(typ.title()) + self.content[0]
152+
self.content[0] = "{}. ".format(realtyp.title()) + self.content[0]
130153
self.state.nested_parse(self.content, 0, section)
131154

132155
node = proof_node()

sphinx_proof/domain.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def generate(self, docnames=None) -> Tuple[Dict[str, Any], bool]:
4545
return content, True
4646

4747
proofs = self.domain.env.proof_list
48-
# {'theorem-0': {'docname': 'start/overview', 'type': 'theorem', 'ids': ['theorem-0'], 'label': 'theorem-0', 'prio': 0, 'nonumber': False}} # noqa: E501
48+
# {'theorem-0': {'docname': 'start/overview', 'realtype': 'theorem', 'countertype': 'theorem', 'ids': ['theorem-0'], 'label': 'theorem-0', 'prio': 0, 'nonumber': False}} # noqa: E501
4949

5050
# name, subtype, docname, typ, anchor, extra, qualifier, description
5151
for anchor, values in proofs.items():
@@ -57,7 +57,7 @@ def generate(self, docnames=None) -> Tuple[Dict[str, Any], bool]:
5757
anchor,
5858
values["docname"],
5959
"",
60-
values["type"],
60+
values["realtype"],
6161
)
6262
)
6363

@@ -157,11 +157,12 @@ def resolve_xref(
157157
if target in contnode[0]:
158158
number = ""
159159
if not env.proof_list[target]["nonumber"]:
160-
typ = env.proof_list[target]["type"]
160+
countertyp = env.proof_list[target]["countertype"]
161161
number = ".".join(
162-
map(str, env.toc_fignumbers[todocname][typ][target])
162+
map(str, env.toc_fignumbers[todocname][countertyp][target])
163163
)
164-
title = nodes.Text(f"{translate(match['type'].title())} {number}")
164+
type_title = translate(match["realtype"].title())
165+
title = nodes.Text(f"{type_title} {number}")
165166
# builder, fromdocname, todocname, targetid, child, title=None
166167
return make_refnode(builder, fromdocname, todocname, target, title)
167168
else:

sphinx_proof/nodes.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
from sphinx.writers.latex import LaTeXTranslator
1414
from sphinx.locale import get_translation
1515

16+
17+
from sphinx.util import logging
18+
19+
logger = logging.getLogger(__name__)
20+
1621
MESSAGE_CATALOG_NAME = "proof"
1722
_ = get_translation(MESSAGE_CATALOG_NAME)
1823

@@ -31,17 +36,18 @@ def visit_enumerable_node(self, node: Node) -> None:
3136

3237

3338
def depart_enumerable_node(self, node: Node) -> None:
34-
typ = node.attributes.get("type", "")
39+
countertyp = node.attributes.get("countertype", "")
40+
realtyp = node.attributes.get("realtype", "")
3541
if isinstance(self, LaTeXTranslator):
36-
number = get_node_number(self, node, typ)
42+
number = get_node_number(self, node, countertyp)
3743
idx = list_rindex(self.body, latex_admonition_start) + 2
38-
self.body.insert(idx, f"{typ.title()} {number}")
44+
self.body.insert(idx, f"{realtyp.title()} {number}")
3945
self.body.append(latex_admonition_end)
4046
else:
4147
# Find index in list of 'Proof #'
42-
number = get_node_number(self, node, typ)
43-
idx = self.body.index(f"{typ} {number} ")
44-
self.body[idx] = f"{_(typ.title())} {number} "
48+
number = get_node_number(self, node, countertyp)
49+
idx = self.body.index(f"{countertyp} {number} ")
50+
self.body[idx] = f"{_(realtyp.title())} {number} "
4551
self.body.append("</div>")
4652

4753

@@ -55,18 +61,18 @@ def visit_unenumerable_node(self, node: Node) -> None:
5561

5662

5763
def depart_unenumerable_node(self, node: Node) -> None:
58-
typ = node.attributes.get("type", "")
64+
realtyp = node.attributes.get("realtype", "")
5965
title = node.attributes.get("title", "")
6066
if isinstance(self, LaTeXTranslator):
6167
idx = list_rindex(self.body, latex_admonition_start) + 2
62-
self.body.insert(idx, f"{typ.title()}")
68+
self.body.insert(idx, f"{realtyp.title()}")
6369
self.body.append(latex_admonition_end)
6470
else:
6571
if title == "":
6672
idx = list_rindex(self.body, '<p class="admonition-title">') + 1
6773
else:
6874
idx = list_rindex(self.body, title)
69-
element = f"<span>{_(typ.title())} </span>"
75+
element = f"<span>{_(realtyp.title())} </span>"
7076
self.body.insert(idx, element)
7177
self.body.append("</div>")
7278

@@ -79,10 +85,10 @@ def depart_proof_node(self, node: Node) -> None:
7985
pass
8086

8187

82-
def get_node_number(self, node: Node, typ) -> str:
88+
def get_node_number(self, node: Node, countertyp) -> str:
8389
"""Get the number for the directive node for HTML."""
8490
ids = node.attributes.get("ids", [])[0]
85-
key = typ
91+
key = countertyp
8692
if isinstance(self, LaTeXTranslator):
8793
docname = find_parent(self.builder.env, node, "section")
8894
fignumbers = self.builder.env.toc_fignumbers.get(
@@ -91,7 +97,7 @@ def get_node_number(self, node: Node, typ) -> str:
9197
else:
9298
fignumbers = self.builder.fignumbers
9399
if self.builder.name == "singlehtml":
94-
key = "%s/%s" % (self.docnames[-1], typ)
100+
key = "%s/%s" % (self.docnames[-1], countertyp)
95101
number = fignumbers.get(key, {}).get(ids, ())
96102
return ".".join(map(str, number))
97103

0 commit comments

Comments
 (0)