From 8fdfb3d9ba513938f6b4945dc90345e65c35e3dc Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst <49307154+douden@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:35:11 +0100 Subject: [PATCH 01/10] Add options for proof directive title formatting (#163) * Add options for proof directive title formatting Introduces new configuration options: proof_title_format, proof_number_weight, and proof_title_weight, allowing users to customize the format and font weight of proof directive titles. Documentation and CSS updated to reflect these changes, and directive logic now applies the user-defined title format. --- CHANGELOG.md | 6 +++ docs/source/options.md | 57 +++++++++++++++++++++++++- sphinx_proof/__init__.py | 24 +++++++++++ sphinx_proof/_static/minimal/proof.css | 9 ++++ sphinx_proof/_static/proof.css | 9 ++++ sphinx_proof/directive.py | 4 +- tests/conftest.py | 6 +-- 7 files changed, 109 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb14503..1c9cfa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added + +- Options for customizing proof directive title format. + ## v0.3.0 (2025-10-20) ### NEW ✨ diff --git a/docs/source/options.md b/docs/source/options.md index 6433d49..f2a2e36 100644 --- a/docs/source/options.md +++ b/docs/source/options.md @@ -1,5 +1,7 @@ # Options +## Minimal color scheme + This package has the option to choose a more **minimal** color scheme. The aim is to create admonitions that are clearly different to the core text with @@ -15,7 +17,7 @@ compared to the current default To enable the `minimal` color scheme you can use the following. -## Jupyter Book Project +### Jupyter Book Project Add `proof_minimal_theme = True` to your `_config.yml` @@ -25,6 +27,57 @@ sphinx: proof_minimal_theme: true ``` -## Sphinx Project +### Sphinx Project Add `proof_minimal_theme = True` to your `conf.py` + +## Title format + +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. + +If no title is provided, only `Name x.y.z` is displayed. + +The font weight of the entire title (`Name x.y.z (Title)` or `Name x.y.z`) is set to `--pst-admonition-font-weight-heading` by default, which commonly results in a semi-bold appearance. + +In the reminder we call the part `Name x.y.z` the "number" and the part `(Title)` the "title". + +You can customize the title format using the `proof_title_format` option: + +- This option allows you to define how the title should be displayed by using `%t` as a placeholder for the user-provided title. +- The default format is ` (%t)`. +- A value of an empty string will result in no title being displayed. +- A `markdown` string can be used to format the title. + - For example, ` *%t*` will emphasize the title and contain no brackets. + +Note that the initial part of the title (i.e., `Name x.y.z`) is not customizable and will always be displayed. + +The font weight of the title can be adjusted using the `proof_title_weight` option: + +- Any valid CSS font-weight value can be used, such as `normal`, `bold`, `bolder`, `lighter`, or numeric values like `400`, `700`, etc. +- Default value is `var(--pst-admonition-font-weight-heading)`. + +The font weight of the number can be adjusted using the `proof_number_weight` option: +- Any valid CSS font-weight value can be used, such as `normal`, `bold`, `bolder`, `lighter`, or numeric values like `400`, `700`, etc. +- Default value is `var(--pst-admonition-font-weight-heading)`. + +### Jupyter Book Project + +Add `proof_title_format`, `proof_number_weight` and/or `proof_title_weight` to your `_config.yml` + +```yaml +sphinx: + config: + proof_title_format: " *%t*" + proof_number_weight: "bold" + proof_title_weight: "normal" +``` + +### Sphinx Project + +Add `proof_title_format`, `proof_number_weight` and/or `proof_title_weight` to your `conf.py` + +```python +proof_title_format = " *%t*" +proof_number_weight = "bold" +proof_title_weight = "normal" +``` diff --git a/sphinx_proof/__init__.py b/sphinx_proof/__init__.py index 61046f3..650caee 100644 --- a/sphinx_proof/__init__.py +++ b/sphinx_proof/__init__.py @@ -76,11 +76,35 @@ def copy_asset_files(app: Sphinx, exc: Union[bool, Exception]): if exc is None: for path in asset_files: copy_asset(path, str(Path(app.outdir).joinpath("_static").absolute())) + # if needed, load css to memory, + # adjust font-weight according to user's setting in config + # and write to output static file + if app.config.proof_number_weight or app.config.proof_title_weight: + # only if at least one of the two options is set + path = str(Path(app.outdir).joinpath("_static", "proof.css").absolute()) + with open(path, "r", encoding="utf-8") as f: + css_content = f.read() + if app.config.proof_number_weight: + css_content = css_content.replace( + "div.proof > p.admonition-title > span.caption-number {\n font-weight: var(--pst-admonition-font-weight-heading);\n}", # noqa: E501 + f"div.proof > p.admonition-title > span.caption-number {{\n font-weight: {app.config.proof_number_weight};\n}}", # noqa: E501 + ) + if app.config.proof_title_weight: + css_content = css_content.replace( + "div.proof > p.admonition-title {\n font-weight: var(--pst-admonition-font-weight-heading);\n}", # noqa: E501 + f"div.proof > p.admonition-title {{\n font-weight: {app.config.proof_title_weight};\n}}", # noqa: E501 + ) + out_path = Path(app.outdir).joinpath("_static", os.path.basename(path)) + with open(out_path, "w", encoding="utf-8") as f: + f.write(css_content) def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value("proof_minimal_theme", False, "html") + app.add_config_value("proof_title_format", " (%t)", "html") + app.add_config_value("proof_number_weight", "", "env") + app.add_config_value("proof_title_weight", "", "env") app.add_css_file("proof.css") app.connect("build-finished", copy_asset_files) diff --git a/sphinx_proof/_static/minimal/proof.css b/sphinx_proof/_static/minimal/proof.css index e4aed47..b8ee2bb 100644 --- a/sphinx_proof/_static/minimal/proof.css +++ b/sphinx_proof/_static/minimal/proof.css @@ -38,6 +38,15 @@ div.proof > .admonition-title::before { content: none; } +/* Set font weights */ +div.proof > p.admonition-title { + font-weight: var(--pst-admonition-font-weight-heading) !important; +} + +div.proof > p.admonition-title > span.caption-number { + font-weight: var(--pst-admonition-font-weight-heading) !important; +} + /********************************************* * Proof * *********************************************/ diff --git a/sphinx_proof/_static/proof.css b/sphinx_proof/_static/proof.css index f975944..5106a40 100644 --- a/sphinx_proof/_static/proof.css +++ b/sphinx_proof/_static/proof.css @@ -23,6 +23,15 @@ div.proof p.admonition-title::before { content: none; } +/* Set font weights */ +div.proof > p.admonition-title { + font-weight: var(--pst-admonition-font-weight-heading); +} + +div.proof > p.admonition-title > span.caption-number { + font-weight: var(--pst-admonition-font-weight-heading); +} + /********************************************* * Proof * *********************************************/ diff --git a/sphinx_proof/directive.py b/sphinx_proof/directive.py index 9d4595b..43209e8 100644 --- a/sphinx_proof/directive.py +++ b/sphinx_proof/directive.py @@ -66,7 +66,9 @@ def run(self) -> List[Node]: title_text = "" if self.arguments != []: - title_text += f" ({self.arguments[0]})" + title_format = self.config.proof_title_format + title_text += title_format.replace("%t", self.arguments[0]) + # title_text += f" ({self.arguments[0]})" textnodes, messages = self.state.inline_text(title_text, self.lineno) diff --git a/tests/conftest.py b/tests/conftest.py index 9efb7f5..c751e14 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,17 @@ import shutil import pytest -from sphinx.testing.path import path +from pathlib import Path pytest_plugins = "sphinx.testing.fixtures" @pytest.fixture def rootdir(tmpdir): - src = path(__file__).parent.abspath() / "books" + src = Path(__file__).parent / "books" dst = tmpdir.join("books") shutil.copytree(src, dst) - books = path(dst) + books = Path(dst) yield books shutil.rmtree(dst) From 27f52c699d4f3476d1a587690ec9a1a44b09a92c Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst <49307154+douden@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:47:14 +0100 Subject: [PATCH 02/10] 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 --- docs/source/conf.py | 2 ++ docs/source/options.md | 55 ++++++++++++++++++++++++++++++++ sphinx_proof/__init__.py | 67 +++++++++++++++++++++++++++++++++++++-- sphinx_proof/directive.py | 51 +++++++++++++++++++++-------- sphinx_proof/domain.py | 11 ++++--- sphinx_proof/nodes.py | 30 +++++++++++------- 6 files changed, 183 insertions(+), 33 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index a0347f4..1d89a4f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -56,3 +56,5 @@ # MyST Parser Configuration myst_enable_extensions = ["dollarmath", "amsmath"] + +prf_realtyp_to_countertyp = {} diff --git a/docs/source/options.md b/docs/source/options.md index f2a2e36..d2c6942 100644 --- a/docs/source/options.md +++ b/docs/source/options.md @@ -31,6 +31,61 @@ sphinx: Add `proof_minimal_theme = True` to your `conf.py` +## Shared numbering + +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. + +### Sphinx Project + +In `conf.py`, e.g. to have a shared counter for all directives: + +``` +prf_realtyp_to_countertyp = { + "axiom": "theorem", + "theorem": "theorem", + "lemma": "theorem", + "algorithm": "theorem", + "definition": "theorem", + "remark": "theorem", + "conjecture": "theorem", + "corollary": "theorem", + "criterion": "theorem", + "example": "theorem", + "property": "theorem", + "observation": "theorem", + "proposition": "theorem", + "assumption": "theorem", + "notation": "theorem", +} +``` + +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. + + +``` +prf_realtyp_to_countertyp = { + "lemma": "theorem", + "conjecture": "theorem", + "corollary": "theorem", + "proposition": "theorem", + "axiom": "definition", + "assumption": "definition", +} +``` + +````{warning} +The association of a counter to a directive is not transitive: Let us consider the following configuration: + +``` +prf_realtyp_to_countertyp = { + "lemma": "theorem", + "conjecture": "lemma", +} +``` + +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). +```` + ## Title format 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. diff --git a/sphinx_proof/__init__.py b/sphinx_proof/__init__.py index 650caee..5f037c0 100644 --- a/sphinx_proof/__init__.py +++ b/sphinx_proof/__init__.py @@ -102,11 +102,13 @@ def copy_asset_files(app: Sphinx, exc: Union[bool, Exception]): def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value("proof_minimal_theme", False, "html") + app.add_config_value("prf_realtyp_to_countertyp", {}, "html") app.add_config_value("proof_title_format", " (%t)", "html") - app.add_config_value("proof_number_weight", "", "env") - app.add_config_value("proof_title_weight", "", "env") + app.add_config_value("proof_number_weight", "", "html") + app.add_config_value("proof_title_weight", "", "html") app.add_css_file("proof.css") + app.connect("config-inited", check_config_values) app.connect("build-finished", copy_asset_files) app.connect("config-inited", init_numfig) app.connect("env-purge-doc", purge_proofs) @@ -142,3 +144,64 @@ def setup(app: Sphinx) -> Dict[str, Any]: "parallel_read_safe": True, "parallel_write_safe": True, } + + +def check_config_values(app: Sphinx, config: Config) -> None: + """Check configuration values.""" + # Check if proof_minimal_theme is boolean + if not isinstance(config.proof_minimal_theme, bool): + logger.warning( + "'proof_minimal_theme' config value must be a boolean. " + "Using default value False." + ) + config.proof_minimal_theme = False + + # Check of prf_realtyp_to_countertyp is a dictionary + if not isinstance(config.prf_realtyp_to_countertyp, dict): + logger.warning( + "'prf_realtyp_to_countertyp' config value must be a dictionary. " + "Using default empty dictionary." + ) + config.prf_realtyp_to_countertyp = {} + # Check if each key and each value in prf_realtyp_to_countertyp + # is a valid proof type + for key, value in config.prf_realtyp_to_countertyp.items(): + if key not in PROOF_TYPES: + logger.warning( + f"Key '{key}' in 'prf_realtyp_to_countertyp' is not " + "a valid proof type. " + "It will be removed." + ) + del config.prf_realtyp_to_countertyp[key] + elif value not in PROOF_TYPES: + logger.warning( + f"Value '{value}' in 'prf_realtyp_to_countertyp' is not " + "a valid proof type. It will be removed." + ) + del config.prf_realtyp_to_countertyp[key] + # Check if proof_title_format is a string + if not isinstance(config.proof_title_format, str): + logger.warning( + "'proof_title_format' config value must be a string." + "Using default value ' (%t)'." + ) + config.proof_title_format = " (%t)" + elif "%t" not in config.proof_title_format: + logger.warning( + "'proof_title_format' config value must contain the " + "substring '%t' to print a title." + ) + # Check if proof_number_weight is a string + if not isinstance(config.proof_number_weight, str): + logger.warning( + "'proof_number_weight' config value must be a string. " + "Using default value ''." + ) + config.proof_number_weight = "" + # Check if proof_title_weight is a string + if not isinstance(config.proof_title_weight, str): + logger.warning( + "'proof_title_weight' config value must be a string. " + "Using default value ''." + ) + config.proof_title_weight = "" diff --git a/sphinx_proof/directive.py b/sphinx_proof/directive.py index 43209e8..2f0b46a 100644 --- a/sphinx_proof/directive.py +++ b/sphinx_proof/directive.py @@ -20,6 +20,25 @@ logger = logging.getLogger(__name__) +DEFAULT_REALTYP_TO_COUNTERTYP = { + "axiom": "axiom", + "theorem": "theorem", + "lemma": "lemma", + "algorithm": "algorithm", + "definition": "definition", + "remark": "remark", + "conjecture": "conjecture", + "corollary": "corollary", + "criterion": "criterion", + "example": "example", + "property": "property", + "observation": "observation", + "proposition": "proposition", + "assumption": "assumption", + "notation": "notation", +} + + class ElementDirective(SphinxDirective): """A custom Sphinx Directive""" @@ -36,13 +55,16 @@ class ElementDirective(SphinxDirective): def run(self) -> List[Node]: env = self.env - typ = self.name.split(":")[1] + realtyp = self.name.split(":")[1] + countertyp = env.config.prf_realtyp_to_countertyp.get( + realtyp, DEFAULT_REALTYP_TO_COUNTERTYP[realtyp] + ) serial_no = env.new_serialno() if not hasattr(env, "proof_list"): env.proof_list = {} # If class in options add to class array - classes, class_name = ["proof", typ], self.options.get("class", []) + classes, class_name = ["proof", realtyp], self.options.get("class", []) if class_name: classes.extend(class_name) @@ -53,15 +75,15 @@ def run(self) -> List[Node]: node_id = f"{label}" else: self.options["noindex"] = True - label = f"{typ}-{serial_no}" - node_id = f"{typ}-{serial_no}" + label = f"{realtyp}-{serial_no}" + node_id = f"{realtyp}-{serial_no}" ids = [node_id] # Duplicate label warning if not label == "" and label in env.proof_list.keys(): path = env.doc2path(env.docname)[:-3] other_path = env.doc2path(env.proof_list[label]["docname"]) - msg = f"duplicate {typ} label '{label}', other instance in {other_path}" + msg = f"duplicate {realtyp} label '{label}', other instance in {other_path}" logger.warning(msg, location=path, color="red") title_text = "" @@ -72,13 +94,13 @@ def run(self) -> List[Node]: textnodes, messages = self.state.inline_text(title_text, self.lineno) - section = nodes.section(classes=[f"{typ}-content"], ids=["proof-content"]) + section = nodes.section(classes=[f"{realtyp}-content"], ids=["proof-content"]) self.state.nested_parse(self.content, self.content_offset, section) if "nonumber" in self.options: node = unenumerable_node() else: - node_type = NODE_TYPES[typ] + node_type = NODE_TYPES[countertyp] node = node_type() node.document = self.state.document @@ -90,17 +112,18 @@ def run(self) -> List[Node]: node["classes"].extend(classes) node["title"] = title_text node["label"] = label - node["type"] = typ + node["countertype"] = countertyp + node["realtype"] = realtyp env.proof_list[label] = { "docname": env.docname, - "type": typ, + "countertype": countertyp, + "realtype": realtyp, "ids": ids, "label": label, "prio": 0, "nonumber": True if "nonumber" in self.options else False, } - return [node] @@ -117,16 +140,16 @@ class ProofDirective(SphinxDirective): } def run(self) -> List[Node]: - typ = self.name.split(":")[1] + realtyp = self.name.split(":")[1] # If class in options add to class array - classes, class_name = ["proof", typ], self.options.get("class", []) + classes, class_name = ["proof", realtyp], self.options.get("class", []) if class_name: classes.extend(class_name) - section = nodes.admonition(classes=classes, ids=[typ]) + section = nodes.admonition(classes=classes, ids=[realtyp]) - self.content[0] = "{}. ".format(typ.title()) + self.content[0] + self.content[0] = "{}. ".format(realtyp.title()) + self.content[0] self.state.nested_parse(self.content, 0, section) node = proof_node() diff --git a/sphinx_proof/domain.py b/sphinx_proof/domain.py index ba8b0ea..7558a0f 100644 --- a/sphinx_proof/domain.py +++ b/sphinx_proof/domain.py @@ -45,7 +45,7 @@ def generate(self, docnames=None) -> Tuple[Dict[str, Any], bool]: return content, True proofs = self.domain.env.proof_list - # {'theorem-0': {'docname': 'start/overview', 'type': 'theorem', 'ids': ['theorem-0'], 'label': 'theorem-0', 'prio': 0, 'nonumber': False}} # noqa: E501 + # {'theorem-0': {'docname': 'start/overview', 'realtype': 'theorem', 'countertype': 'theorem', 'ids': ['theorem-0'], 'label': 'theorem-0', 'prio': 0, 'nonumber': False}} # noqa: E501 # name, subtype, docname, typ, anchor, extra, qualifier, description for anchor, values in proofs.items(): @@ -57,7 +57,7 @@ def generate(self, docnames=None) -> Tuple[Dict[str, Any], bool]: anchor, values["docname"], "", - values["type"], + values["realtype"], ) ) @@ -157,11 +157,12 @@ def resolve_xref( if target in contnode[0]: number = "" if not env.proof_list[target]["nonumber"]: - typ = env.proof_list[target]["type"] + countertyp = env.proof_list[target]["countertype"] number = ".".join( - map(str, env.toc_fignumbers[todocname][typ][target]) + map(str, env.toc_fignumbers[todocname][countertyp][target]) ) - title = nodes.Text(f"{translate(match['type'].title())} {number}") + type_title = translate(match["realtype"].title()) + title = nodes.Text(f"{type_title} {number}") # builder, fromdocname, todocname, targetid, child, title=None return make_refnode(builder, fromdocname, todocname, target, title) else: diff --git a/sphinx_proof/nodes.py b/sphinx_proof/nodes.py index 96bbff7..76977e4 100644 --- a/sphinx_proof/nodes.py +++ b/sphinx_proof/nodes.py @@ -13,6 +13,11 @@ from sphinx.writers.latex import LaTeXTranslator from sphinx.locale import get_translation + +from sphinx.util import logging + +logger = logging.getLogger(__name__) + MESSAGE_CATALOG_NAME = "proof" _ = get_translation(MESSAGE_CATALOG_NAME) @@ -31,17 +36,18 @@ def visit_enumerable_node(self, node: Node) -> None: def depart_enumerable_node(self, node: Node) -> None: - typ = node.attributes.get("type", "") + countertyp = node.attributes.get("countertype", "") + realtyp = node.attributes.get("realtype", "") if isinstance(self, LaTeXTranslator): - number = get_node_number(self, node, typ) + number = get_node_number(self, node, countertyp) idx = list_rindex(self.body, latex_admonition_start) + 2 - self.body.insert(idx, f"{typ.title()} {number}") + self.body.insert(idx, f"{realtyp.title()} {number}") self.body.append(latex_admonition_end) else: # Find index in list of 'Proof #' - number = get_node_number(self, node, typ) - idx = self.body.index(f"{typ} {number} ") - self.body[idx] = f"{_(typ.title())} {number} " + number = get_node_number(self, node, countertyp) + idx = self.body.index(f"{countertyp} {number} ") + self.body[idx] = f"{_(realtyp.title())} {number} " self.body.append("") @@ -55,18 +61,18 @@ def visit_unenumerable_node(self, node: Node) -> None: def depart_unenumerable_node(self, node: Node) -> None: - typ = node.attributes.get("type", "") + realtyp = node.attributes.get("realtype", "") title = node.attributes.get("title", "") if isinstance(self, LaTeXTranslator): idx = list_rindex(self.body, latex_admonition_start) + 2 - self.body.insert(idx, f"{typ.title()}") + self.body.insert(idx, f"{realtyp.title()}") self.body.append(latex_admonition_end) else: if title == "": idx = list_rindex(self.body, '

') + 1 else: idx = list_rindex(self.body, title) - element = f"{_(typ.title())} " + element = f"{_(realtyp.title())} " self.body.insert(idx, element) self.body.append("") @@ -79,10 +85,10 @@ def depart_proof_node(self, node: Node) -> None: pass -def get_node_number(self, node: Node, typ) -> str: +def get_node_number(self, node: Node, countertyp) -> str: """Get the number for the directive node for HTML.""" ids = node.attributes.get("ids", [])[0] - key = typ + key = countertyp if isinstance(self, LaTeXTranslator): docname = find_parent(self.builder.env, node, "section") fignumbers = self.builder.env.toc_fignumbers.get( @@ -91,7 +97,7 @@ def get_node_number(self, node: Node, typ) -> str: else: fignumbers = self.builder.fignumbers if self.builder.name == "singlehtml": - key = "%s/%s" % (self.docnames[-1], typ) + key = "%s/%s" % (self.docnames[-1], countertyp) number = fignumbers.get(key, {}).get(ids, ()) return ".".join(map(str, number)) From b337c21675464224a4418beb9e70d3b6dc8e4127 Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Sun, 25 Jan 2026 11:08:42 +0100 Subject: [PATCH 03/10] Add notation style and unify proof CSS variables Introduced CSS variables and styles for 'notation' admonitions in both minimal and main proof.css. Standardized and expanded the use of CSS custom properties for all proof-related admonition types, improving maintainability and visual consistency. --- sphinx_proof/_static/minimal/proof.css | 16 ++- sphinx_proof/_static/proof.css | 131 ++++++++++++++++--------- 2 files changed, 102 insertions(+), 45 deletions(-) diff --git a/sphinx_proof/_static/minimal/proof.css b/sphinx_proof/_static/minimal/proof.css index b8ee2bb..2f2054c 100644 --- a/sphinx_proof/_static/minimal/proof.css +++ b/sphinx_proof/_static/minimal/proof.css @@ -12,9 +12,10 @@ --corollary-border-color: #c500c1; --example-border-color: #f9377b; --property-border-color: #fdf914; - --obseration-border-color: #7707ff; + --observation-border-color: #7707ff; --proposition-border-color: #4f7aa8; --assumption-border-color: #07fffb; + --notation-border-color: rgb(0, 255, 200); /* --note-title-color: rgba(68,138,255,.1); --note-border-color: #007bff; @@ -218,6 +219,7 @@ div.proposition { div.proposition > .admonition-title { background-color: transparent; } + /********************************************* * Assumption * *********************************************/ @@ -229,3 +231,15 @@ div.assumption { div.assumption > .admonition-title { background-color: transparent; } + +/********************************************* +* Notation * +*********************************************/ +div.notation { + border-color: var(--notation-border-color); + background-color: none; +} + +div.notation > .admonition-title { + background-color: transparent; +} diff --git a/sphinx_proof/_static/proof.css b/sphinx_proof/_static/proof.css index 5106a40..f98b254 100644 --- a/sphinx_proof/_static/proof.css +++ b/sphinx_proof/_static/proof.css @@ -2,16 +2,46 @@ * Variables * *********************************************/ :root { - --note-title-color: rgba(68,138,255,.1); - --note-border-color: #007bff; + /* --note-title-color: rgba(68,138,255,.1); + --note-border-color: rgb(0, 123, 255); --warning-title-color: rgba(220,53,69,.1); - --warning-border-color: #dc3545; + --warning-border-color: rgb(220, 56, 72); --hint-title-color: rgba(255,193,7,.2); - --hint-border-color: #ffc107; + --hint-border-color: rgb(255, 193, 7); --caution-title-color: rgba(253,126,20,.1); - --caution-border-color: #fd7e14; + --caution-border-color: #fd7e14; */ --grey-title-color: rgba(204,204,204,.2); --grey-border-color: #ccc; + + --theorem-border-color: rgba(68,138,255); + --axiom-border-color: rgb(255, 193, 7); + --criterion-border-color: rgb(253, 126, 20); + --lemma-border-color: rgb(54, 207, 3); + --definition-border-color: rgb(0, 123, 255); + --remark-border-color: rgb(121, 255, 111); + --conjecture-border-color: rgb(119, 95, 21); + --corollary-border-color: rgb(197, 0, 193); + --example-border-color: rgb(249, 55, 123); + --property-border-color: rgb(253, 249, 20); + --observation-border-color: rgb(119, 7, 255); + --proposition-border-color: rgb(79, 122, 168); + --assumption-border-color: rgb(7, 255, 251); + --notation-border-color: rgb(0, 255, 200); + + --theorem-title-color: rgba(68,138,255,.1); + --axiom-title-color: rgb(255, 193, 7,.1); + --criterion-title-color: rgb(253, 126, 20,.1); + --lemma-title-color: rgb(54, 207, 3,.1); + --definition-title-color: rgb(0, 123, 255,.1); + --remark-title-color: rgb(121, 255, 111,.1); + --conjecture-title-color: rgb(119, 95, 21,.1); + --corollary-title-color: rgb(197, 0, 193,.1); + --example-title-color: rgb(249, 55, 123,.1); + --property-title-color: rgb(253, 249, 20,.1); + --observation-title-color: rgb(119, 7, 255,.1); + --proposition-title-color: rgb(79, 122, 168,.1); + --assumption-title-color: rgb(7, 255, 251,.1); + --notation-title-color: rgb(0, 255, 200,.1); } /********************************************* @@ -45,96 +75,96 @@ div#proof{ * Theorem * *********************************************/ div.theorem { - border-color: var(--note-border-color); - background-color: var(--note-title-color); + border-color: var(--theorem-border-color); + background-color: var(--theorem-title-color); } div.theorem p.admonition-title { - background-color: var(--note-title-color); + background-color: var(--theorem-title-color); } /********************************************* * Axiom * *********************************************/ div.axiom { - border-color: var(--hint-border-color); - background-color: var(--hint-title-color); + border-color: var(--axiom-border-color); + background-color: var(--axiom-title-color); } div.axiom p.admonition-title { - background-color: var(--hint-title-color); + background-color: var(--axiom-title-color); } /********************************************* * Criterion * *********************************************/ div.criterion { - border-color: var(--caution-border-color); - background-color: var(--caution-title-color); + border-color: var(--criterion-border-color); + background-color: var(--criterion-title-color); } div.criterion p.admonition-title { - background-color: var(--caution-title-color); + background-color: var(--criterion-title-color); } /********************************************* * Lemma * *********************************************/ div.lemma { - border-color: var(--hint-border-color); - background-color: var(--hint-title-color); + border-color: var(--lemma-border-color); + background-color: var(--lemma-title-color); } div.lemma p.admonition-title { - background-color: var(--hint-title-color); + background-color: var(--lemma-title-color); } /********************************************* * Definition * *********************************************/ div.definition { - border-color: var(--note-border-color); - background-color: var(--note-title-color); + border-color: var(--definition-border-color); + background-color: var(--definition-title-color); } div.definition p.admonition-title { - background-color: var(--note-title-color); + background-color: var(--definition-title-color); } /********************************************* * Remark * *********************************************/ div.remark { - border-color: var(--warning-border-color); - background-color: var(--warning-title-color); + border-color: var(--remark-border-color); + background-color: var(--remark-title-color); } div.remark p.admonition-title { - background-color: var(--warning-title-color); + background-color: var(--remark-title-color); } /********************************************* * Conjecture * *********************************************/ div.conjecture { - border-color: var(--hint-border-color); - background-color: var(--hint-title-color); + border-color: var(--conjecture-border-color); + background-color: var(--conjecture-title-color); } div.conjecture p.admonition-title { - background-color: var(--hint-title-color); + background-color: var(--conjecture-title-color); } /********************************************* * Corollary * *********************************************/ div.corollary { - border-color: var(--caution-border-color); - background-color: var(--caution-title-color); + border-color: var(--corollary-border-color); + background-color: var(--corollary-title-color); } div.corollary p.admonition-title { - background-color: var(--caution-title-color); + background-color: var(--corollary-title-color); } /********************************************* @@ -160,57 +190,70 @@ div.algorithm div.section { * Example * *********************************************/ div.example { - border-color: var(--hint-border-color); - background-color: none; + border-color: var(--example-border-color); + background-color: var(--example-title-color); } div.example p.admonition-title { - background-color: transparent; + background-color: var(--example-title-color); } /********************************************* * Property * *********************************************/ div.property { - border-color: var(--caution-border-color); - background-color: var(--caution-title-color); + border-color: var(--property-border-color); + background-color: var(--property-title-color); } div.property p.admonition-title { - background-color: var(--caution-title-color); + background-color: var(--property-title-color); } /********************************************* * Observation * *********************************************/ div.observation { - border-color: var(--hint-border-color); - background-color: var(--hint-title-color); + border-color: var(--observation-border-color); + background-color: var(--observation-title-color); } div.observation p.admonition-title { - background-color: var(--hint-title-color); + background-color: var(--observation-title-color); } /********************************************* * Proposition * *********************************************/ div.proposition { - border-color: var(--note-border-color); - background-color: var(--note-title-color); + border-color: var(--proposition-border-color); + background-color: var(--proposition-title-color); } div.proposition p.admonition-title { - background-color: var(--note-title-color); + background-color: var(--proposition-title-color); } + /********************************************* * Assumption * *********************************************/ div.assumption { - border-color: var(--hint-border-color); - background-color: var(--hint-title-color); + border-color: var(--assumption-border-color); + background-color: var(--assumption-title-color); } div.assumption p.admonition-title { - background-color: var(--hint-title-color); + background-color: var(--assumption-title-color); } + +/********************************************* +* Notation * +*********************************************/ +div.notation { + border-color: var(--notation-border-color); + background-color: var(--notation-title-color); +} + +div.notation p.admonition-title { + background-color: var(--notation-title-color); +} \ No newline at end of file From 6f33744544b7596a30beba5b8807491d642b77a7 Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Sun, 25 Jan 2026 11:13:58 +0100 Subject: [PATCH 04/10] Add documentation for notation directive Introduces a new section describing the `prf:notation` directive, its options, usage examples, and referencing notations in the documentation. --- docs/source/syntax.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/source/syntax.md b/docs/source/syntax.md index a73c848..cbf414c 100644 --- a/docs/source/syntax.md +++ b/docs/source/syntax.md @@ -749,6 +749,43 @@ This is a dummy assumption directive. You can refer to an assumption using the `{prf:ref}` role like: ```{prf:ref}`my-assumption` ```, which will replace the reference with the assumption number like so: {prf:ref}`my-assumption`. When an explicit text is provided, this caption will serve as the title of the reference. +(syntax:notation)= +### Notations + +A notation directive can be included using the `prf:notation` pattern. The directive is enumerated by default and can take in an optional title argument. The following options are also supported: + +* `label` : text + + A unique identifier for your notation that you can use to reference it with `{prf:ref}`. Cannot contain spaces or special characters. +* `class` : text + + Value of the notation’s class attribute which can be used to add custom CSS or JavaScript. +* `nonumber` : flag (empty) + + Turns off notation auto numbering. + +**Example** + +```{prf:notation} +:label: my-notation + +This is a dummy notation directive. +``` + +**MyST Syntax** + +``````md +```{prf:notation} +:label: my-notation + +This is a dummy notation directive. +``` +`````` + +#### Referencing Notations + +You can refer to a notation using the `{prf:ref}` role like: ```{prf:ref}`my-notation` ```, which will replace the reference with the notation number like so: {prf:ref}`my-notation`. When an explicit text is provided, this caption will serve as the title of the reference. + ## How to Hide Content Directive content can be hidden using the `dropdown` class which is available through [sphinx-togglebutton](https://sphinx-togglebutton.readthedocs.io/en/latest/). If your project utilizes the [MyST-NB](https://myst-nb.readthedocs.io/en/latest/) extension, there is no need to activate `sphinx-togglebutton` since it is already bundled with `MyST-NB`. From b15c7c09adefe85e0fdb8dea6dc460bd0f51fb1f Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Sun, 25 Jan 2026 11:19:59 +0100 Subject: [PATCH 05/10] Add notation directive to documentation index Included the [notation](syntax:notation) directive in the list of supported directives on the documentation index page. --- docs/source/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.md b/docs/source/index.md index c0924b8..0e24000 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -22,7 +22,7 @@ for producing [proof](syntax:proof), [theorem](syntax:theorem), [axiom](syntax:a [definition](syntax:definition), [criterion](syntax:criterion), [remark](syntax:remark), [conjecture](syntax:conjecture),[corollary](syntax:corollary), [algorithm](syntax:algorithm), [example](syntax:example), [property](syntax:property), [observation](syntax:observation), -[proposition](syntax:proposition) and [assumption](syntax:assumption) directives. +[proposition](syntax:proposition), [assumption](syntax:assumption) and [notation](syntax:notation) directives. **Features**: From 4d5c386a4f76a8cb66704ff6f031ba5b7b82df89 Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Sun, 25 Jan 2026 11:21:28 +0100 Subject: [PATCH 06/10] Update changelog and README for new features and fixes Added details about shared numbering for directive groups, fixed CSS inconsistencies, and documented the notation directive in CHANGELOG.md. Updated README.md to include the notation directive, mention shared numbering, and clarify styling options. --- CHANGELOG.md | 8 +++++++- README.md | 8 +++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9cfa9..59ffd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ ### Added -- Options for customizing proof directive title format. +- Options for customizing proof directive title format. [#163](https://github.com/executablebooks/sphinx-proof/pull/163) +- Possibility for shared numbering of groups of directives, fixing [\#64](https://github.com/executablebooks/sphinx-proof/issues/64). [\#https://github.com/executablebooks/sphinx-proof/pull/161](https://github.com/executablebooks/sphinx-proof/pull/161) + +### Fixed + +- Inconsistencies and missing colors within CSS styles. [b337c21](https://github.com/executablebooks/sphinx-proof/commit/b337c21675464224a4418beb9e70d3b6dc8e4127) +- Missing documentation for notation directive. [6f33744](https://github.com/executablebooks/sphinx-proof/commit/6f33744544b7596a30beba5b8807491d642b77a7) and [b15c7c0](https://github.com/executablebooks/sphinx-proof/commit/b15c7c09adefe85e0fdb8dea6dc460bd0f51fb1f) ## v0.3.0 (2025-10-20) diff --git a/README.md b/README.md index 2696c10..e62f5e0 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,18 @@ This package contains a [Sphinx](http://www.sphinx-doc.org/) extension for producing proof, theorem, axiom, lemma, definition, criterion, remark, conjecture, -corollary, algorithm, example, property, observation, proposition and assumption directives. +corollary, algorithm, example, property, observation, proposition, assumption and notation directives. ## Features - **15 directive types** for mathematical proofs and theorems - **Automatic numbering** of directives + - optional shared numbering of groups of directives - **Cross-referencing** support via `prf:ref` role - **33 languages supported** - Complete translations for all directive types in English plus 32 additional languages (Arabic, Bengali, Bulgarian, Chinese, Czech, Danish, Dutch, Finnish, French, German, Greek, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Malay, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Spanish, Swedish, Thai, Turkish, Ukrainian, Urdu, Vietnamese) -- **Customizable styling** with multiple theme options - +- **Customizable styling** with: + - two theme options + - directive title format customization ## Get started From c3e03012c61c1d7a740d4ee24653abbfbf4d718a Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Sun, 25 Jan 2026 11:30:45 +0100 Subject: [PATCH 07/10] Add newline at end of proof.css file Appended a newline character to the end of sphinx_proof/_static/proof.css to follow POSIX file standards and improve compatibility with some tools. --- sphinx_proof/_static/proof.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_proof/_static/proof.css b/sphinx_proof/_static/proof.css index f98b254..7462ec8 100644 --- a/sphinx_proof/_static/proof.css +++ b/sphinx_proof/_static/proof.css @@ -256,4 +256,4 @@ div.notation { div.notation p.admonition-title { background-color: var(--notation-title-color); -} \ No newline at end of file +} From 0d274ece1c1dd65dd3832c8244704c41246e340d Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Mon, 26 Jan 2026 11:29:22 +0100 Subject: [PATCH 08/10] Fix nesting of unnumbered directives in nodes.py Improves handling of nested unenumerable nodes by correctly identifying the appropriate occurrence of '

' in the body. Updates the list_rindex function to support skipping occurrences, addressing issue #165. --- CHANGELOG.md | 1 + sphinx_proof/nodes.py | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ffd5d..4abe27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Inconsistencies and missing colors within CSS styles. [b337c21](https://github.com/executablebooks/sphinx-proof/commit/b337c21675464224a4418beb9e70d3b6dc8e4127) - Missing documentation for notation directive. [6f33744](https://github.com/executablebooks/sphinx-proof/commit/6f33744544b7596a30beba5b8807491d642b77a7) and [b15c7c0](https://github.com/executablebooks/sphinx-proof/commit/b15c7c09adefe85e0fdb8dea6dc460bd0f51fb1f) +- Nesting of unnumbered directives, fixing [\#165](https://github.com/executablebooks/sphinx-proof/issues/165). ## v0.3.0 (2025-10-20) diff --git a/sphinx_proof/nodes.py b/sphinx_proof/nodes.py index 76977e4..bb93976 100644 --- a/sphinx_proof/nodes.py +++ b/sphinx_proof/nodes.py @@ -69,7 +69,22 @@ def depart_unenumerable_node(self, node: Node) -> None: self.body.append(latex_admonition_end) else: if title == "": - idx = list_rindex(self.body, '

') + 1 + # because of nesting, first find out the correct occurrence + logger.info("Departing unenumerable node with empty title", color="blue") + logger.info(f"Node: {node.pformat()}", color="green") + closer_found = False + skip = 0 + while not closer_found: + idx = list_rindex(self.body, '

', skip) + 1 + closer = self.body[idx] + logger.info(f"Closer found: {closer}", color="yellow") + if "

" in closer: + closer_found = True + else: + skip += 1 + # Here the issue is generated + # a closing

should be found. + else: idx = list_rindex(self.body, title) element = f"{_(realtyp.title())} " @@ -122,11 +137,15 @@ def find_parent(env, node, parent_tag): return None -def list_rindex(li, x) -> int: - """Getting the last occurence of an item in a list.""" +def list_rindex(li, x, skip=0) -> int: + """Getting the last occurrence of an item in a list.""" + """Skipping the first skip occurrences from the end.""" for i in reversed(range(len(li))): if li[i] == x: - return i + if skip == 0: + return i + else: + skip -= 1 raise ValueError("{} is not in list".format(x)) From f369892a6e4b3172d1bf62a55e2048fe97cd869a Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Mon, 26 Jan 2026 20:50:15 +0100 Subject: [PATCH 09/10] Improve handling of nonumber nodes and title IDs Adds IDs to title nodes when the 'nonumber' option is set, enabling more reliable identification of titles in HTML output. Refactors the depart_unenumerable_node function to use these IDs for inserting captions, simplifying the logic and improving robustness. --- sphinx_proof/directive.py | 7 ++++++- sphinx_proof/nodes.py | 26 +++++--------------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/sphinx_proof/directive.py b/sphinx_proof/directive.py index 2f0b46a..f6dd012 100644 --- a/sphinx_proof/directive.py +++ b/sphinx_proof/directive.py @@ -104,7 +104,12 @@ def run(self) -> List[Node]: node = node_type() node.document = self.state.document - node += nodes.title(title_text, "", *textnodes) + node_title = nodes.title(title_text, "", *textnodes) + if "nonumber" in self.options: + # add ids to the title node for nonumber + # nodes for later searching + node_title["ids"].extend(ids) + node += node_title node += section # Set node attributes diff --git a/sphinx_proof/nodes.py b/sphinx_proof/nodes.py index bb93976..d77b257 100644 --- a/sphinx_proof/nodes.py +++ b/sphinx_proof/nodes.py @@ -62,32 +62,16 @@ def visit_unenumerable_node(self, node: Node) -> None: def depart_unenumerable_node(self, node: Node) -> None: realtyp = node.attributes.get("realtype", "") - title = node.attributes.get("title", "") + id = node.attributes.get("ids", [""])[0] if isinstance(self, LaTeXTranslator): idx = list_rindex(self.body, latex_admonition_start) + 2 self.body.insert(idx, f"{realtyp.title()}") self.body.append(latex_admonition_end) else: - if title == "": - # because of nesting, first find out the correct occurrence - logger.info("Departing unenumerable node with empty title", color="blue") - logger.info(f"Node: {node.pformat()}", color="green") - closer_found = False - skip = 0 - while not closer_found: - idx = list_rindex(self.body, '

', skip) + 1 - closer = self.body[idx] - logger.info(f"Closer found: {closer}", color="yellow") - if "

" in closer: - closer_found = True - else: - skip += 1 - # Here the issue is generated - # a closing

should be found. - - else: - idx = list_rindex(self.body, title) - element = f"{_(realtyp.title())} " + # use the id to find the correct title location + search_str = f'

' + idx = list_rindex(self.body, search_str) + 1 + element = f'{_(realtyp.title())} ' self.body.insert(idx, element) self.body.append("") From 10937bdd3ad8171dcdd292c4be601587bcd6fd35 Mon Sep 17 00:00:00 2001 From: Dennis den Ouden-van der Horst Date: Mon, 26 Jan 2026 21:02:40 +0100 Subject: [PATCH 10/10] Add caption-number span and id to algorithm title Updated the algorithm admonition title to include a 'caption-number' span and set the id attribute for improved structure and accessibility. --- tests/test_html/algorithm/_algo_nonumber.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_html/algorithm/_algo_nonumber.html b/tests/test_html/algorithm/_algo_nonumber.html index d65590b..7fa6d0f 100644 --- a/tests/test_html/algorithm/_algo_nonumber.html +++ b/tests/test_html/algorithm/_algo_nonumber.html @@ -1,5 +1,5 @@

-

Algorithm (Test algorithm directive)

+

Algorithm (Test algorithm directive)

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.