sphinx-hackerrank 2026.10.30.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ """Sphinx extension for portable HackerRank interviewer mini-maps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sphinx_hackerrank.builder import HackerRankBuilder
8
+ from sphinx_hackerrank.sections import MiniMapSection, discard_guide_cache, simplify_mini_map
9
+
10
+ if TYPE_CHECKING:
11
+ from sphinx.application import Sphinx
12
+
13
+ __version__ = "2026.10.30.1"
14
+
15
+
16
+ def setup(app: Sphinx) -> dict[str, object]:
17
+ """Register the builder, directive, and project-independent settings."""
18
+ app.setup_extension("sphinx_jinja2")
19
+ app.setup_extension("sphinx_toolbox.collapse")
20
+ app.add_config_value("hackerrank_manifest_pattern", "**/*-mini-map.yml", "env", types=str)
21
+ app.add_config_value("hackerrank_languages", {}, "env", types=dict)
22
+ app.add_config_value("hackerrank_jinja_context", "languages", "env", types=str)
23
+ app.add_config_value("hackerrank_source_url", None, "html", types=(str, type(None)))
24
+ app.add_config_value("hackerrank_source_suffix", ".html", "html", types=str)
25
+ app.add_config_value("hackerrank_source_anchors", True, "html", types=bool)
26
+ app.add_config_value("hackerrank_exclude_admonitions", True, "html", types=bool)
27
+ app.add_builder(HackerRankBuilder)
28
+ app.add_directive("mini-map-section", MiniMapSection)
29
+ app.connect("doctree-read", discard_guide_cache)
30
+ app.connect("doctree-resolved", simplify_mini_map)
31
+ return {"version": __version__, "parallel_read_safe": True, "parallel_write_safe": True}
@@ -0,0 +1,117 @@
1
+ """Build HackerRank fragments using Sphinx's standard HTML renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from html import escape
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from sphinx.builders.html import StandaloneHTMLBuilder
10
+
11
+ from sphinx_hackerrank.html import fragment_path, render_fragment
12
+ from sphinx_hackerrank.targets import load_targets
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterator
16
+
17
+
18
+ class HackerRankBuilder(StandaloneHTMLBuilder):
19
+ """Add standalone language fragments to the normal HTML output."""
20
+
21
+ name = "hackerrank"
22
+ format = "html"
23
+
24
+ def init(self) -> None:
25
+ """Validate all question mappings before writing any output."""
26
+ super().init()
27
+ self.mini_map_targets = load_targets(
28
+ source_dir=Path(self.srcdir), pattern=self.config.hackerrank_manifest_pattern
29
+ )
30
+
31
+ def get_outdated_docs(self) -> Iterator[str]:
32
+ """Rebuild missing fragments and changed mappings or
33
+ presentation.
34
+ """
35
+ self.mini_map_targets = load_targets(
36
+ source_dir=Path(self.srcdir), pattern=self.config.hackerrank_manifest_pattern
37
+ )
38
+ outdated = set(super().get_outdated_docs())
39
+ if not (Path(self.outdir) / "mini-maps" / "index.html").is_file():
40
+ outdated.update(self.mini_map_targets)
41
+ for docname, targets in self.mini_map_targets.items():
42
+ newest_dependency = max(
43
+ path.stat().st_mtime_ns
44
+ for path in (
45
+ targets[0].metadata_path,
46
+ Path(__file__),
47
+ Path(__file__).with_name(name="html.py"),
48
+ Path(__file__).parent / "static" / "mini_map.css",
49
+ )
50
+ )
51
+ outputs = (
52
+ fragment_path(output_dir=Path(self.outdir), docname=docname, language=target.language)
53
+ for target in targets
54
+ )
55
+ if any(not output.is_file() or output.stat().st_mtime_ns < newest_dependency for output in outputs):
56
+ outdated.add(docname)
57
+ return iter(sorted(outdated))
58
+
59
+ def handle_page(
60
+ self,
61
+ pagename: str,
62
+ addctx: dict[str, Any],
63
+ templatename: str = "page.html",
64
+ *,
65
+ outfilename: Path | None = None,
66
+ event_arg: object = None,
67
+ ) -> None:
68
+ """Export the rendered body before the HTML theme adds chrome."""
69
+ for target in self.mini_map_targets.get(pagename, []):
70
+ output_path = fragment_path(output_dir=Path(self.outdir), docname=pagename, language=target.language)
71
+ output_path.parent.mkdir(parents=True, exist_ok=True)
72
+ output_path.write_text(
73
+ data=render_fragment(
74
+ html=addctx["body"],
75
+ language=target.language,
76
+ docname=pagename,
77
+ source_url=self.config.hackerrank_source_url,
78
+ source_suffix=self.config.hackerrank_source_suffix,
79
+ source_anchors=self.config.hackerrank_source_anchors,
80
+ ),
81
+ encoding="utf-8",
82
+ )
83
+ super().handle_page(
84
+ pagename=pagename,
85
+ addctx=addctx,
86
+ templatename=templatename,
87
+ outfilename=outfilename,
88
+ event_arg=event_arg,
89
+ )
90
+
91
+ def finish(self) -> None:
92
+ """Add a local preview index linking to each standalone mini-
93
+ map.
94
+ """
95
+ super().finish()
96
+ preview_dir = Path(self.outdir) / "mini-maps"
97
+ preview_dir.mkdir(parents=True, exist_ok=True)
98
+ links = "\n".join(
99
+ f'<li><a href="{escape(s=docname.removesuffix("-mini-map"))}/{target.language}.html">'
100
+ f"{escape(s=target.name)}</a></li>"
101
+ for docname, targets in self.mini_map_targets.items()
102
+ for target in targets
103
+ )
104
+ (preview_dir / "index.html").write_text(
105
+ data=(
106
+ '<!doctype html>\n<html lang="en"><head><meta charset="utf-8">'
107
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
108
+ "<title>HackerRank mini-maps</title>"
109
+ "<style>body{font:16px/1.5 Arial,sans-serif;margin:2rem;max-width:60rem}"
110
+ "li{margin:.5rem 0}</style></head><body><main>"
111
+ "<h1>HackerRank mini-maps</h1>"
112
+ "<p>Choose a question and language to preview its interviewer mini-map.</p>"
113
+ "<p>These previews use the same HTML published to HackerRank interviewer notes.</p>"
114
+ f"<ul>{links}</ul></main></body></html>\n"
115
+ ),
116
+ encoding="utf-8",
117
+ )
@@ -0,0 +1,54 @@
1
+ """Render standalone HTML without a theme or external assets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import posixpath
6
+ from importlib.resources import files
7
+ from pathlib import Path
8
+ from urllib.parse import urljoin, urlsplit
9
+
10
+ from bs4 import BeautifulSoup
11
+
12
+
13
+ def fragment_path(*, output_dir: Path, docname: str, language: str) -> Path:
14
+ """Locate a language fragment in a builder output directory."""
15
+ return output_dir / "mini-maps" / docname.removesuffix("-mini-map") / f"{language}.html"
16
+
17
+
18
+ def render_fragment(
19
+ *,
20
+ html: str,
21
+ language: str,
22
+ docname: str = "mini-map",
23
+ source_url: str | None = None,
24
+ source_suffix: str = ".html",
25
+ source_anchors: bool = True,
26
+ ) -> str:
27
+ """Extract a language view, retaining exact code and portable links."""
28
+ soup = BeautifulSoup(html, "html.parser")
29
+ containers = soup.select(f".mini-map-{language}")
30
+ if len(containers) != 1:
31
+ raise ValueError(f"Expected one mini-map for {language}, found {len(containers)}")
32
+ container = containers[0]
33
+ for link in container.select("a.headerlink"):
34
+ link.decompose()
35
+ output = fragment_path(output_dir=Path(), docname=docname, language=language)
36
+ for link in container.select("a[href]"):
37
+ url = urlsplit(str(link["href"]))
38
+ if not url.scheme and not url.netloc and url.path.endswith(".html"):
39
+ target = posixpath.normpath(posixpath.join(posixpath.dirname(docname), url.path))
40
+ if source_url is None:
41
+ destination = posixpath.relpath(target, output.parent.as_posix())
42
+ else:
43
+ destination = urljoin(source_url.rstrip("/") + "/", target.removesuffix(".html") + source_suffix)
44
+ if source_anchors and url.fragment:
45
+ destination += "#" + url.fragment
46
+ link["href"] = destination
47
+ for pre in container.find_all("pre"):
48
+ code = soup.new_tag("code")
49
+ code.string = pre.get_text()
50
+ pre.clear()
51
+ pre.append(code)
52
+ fragment = str(container).encode("ascii", errors="xmlcharrefreplace").decode("ascii")
53
+ style = files("sphinx_hackerrank").joinpath("static/mini_map.css").read_text(encoding="utf-8")
54
+ return f'<style type="text/css">{style}</style>\n{fragment}\n'
File without changes
@@ -0,0 +1,182 @@
1
+ """Reuse parsed guide sections in language-specific Sphinx mini-maps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import copy
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, ClassVar
8
+
9
+ from docutils import nodes
10
+ from docutils.parsers.rst import Parser, directives
11
+ from docutils.transforms.references import (
12
+ AnonymousHyperlinks,
13
+ ExternalTargets,
14
+ IndirectHyperlinks,
15
+ InternalTargets,
16
+ PropagateTargets,
17
+ Substitutions,
18
+ )
19
+ from docutils.utils import new_document
20
+ from sphinx.transforms import DefaultSubstitutions
21
+ from sphinx.util.docutils import SphinxDirective
22
+ from sphinx_toolbox.collapse import CollapseNode, CollapseSummaryNode
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable
26
+
27
+ from sphinx.application import Sphinx
28
+
29
+
30
+ def select_section(*, document: nodes.document, selector: str) -> nodes.section:
31
+ """Resolve an exact heading path or a standard RST section label."""
32
+ sections = list(document.findall(condition=nodes.section))
33
+ if selector.startswith("#"):
34
+ label = nodes.fully_normalize_name(name=selector[1:])
35
+ matches = [section for section in sections if label in section["names"] or label in section["ids"]]
36
+ else:
37
+ parts = [part.strip() for part in selector.split(sep="/")]
38
+ matches = [section for section in sections if section[0].astext() == parts[0]]
39
+ for part in parts[1:]:
40
+ matches = [
41
+ child
42
+ for section in matches
43
+ for child in section.children
44
+ if isinstance(child, nodes.section) and child[0].astext() == part
45
+ ]
46
+ if len(matches) != 1:
47
+ msg = f"Expected one guide section for {selector!r}, found {len(matches)}"
48
+ raise ValueError(msg)
49
+ return matches[0]
50
+
51
+
52
+ def _rubric(title: nodes.Node) -> nodes.rubric:
53
+ assert isinstance(title, nodes.title)
54
+ rubric = nodes.rubric(rawsource=title.rawsource)
55
+ rubric += title.deepcopy().children
56
+ return rubric
57
+
58
+
59
+ def section_content(*, section: nodes.section, guide: str, language: str, include_heading: bool) -> list[nodes.Node]:
60
+ """Copy selected nodes without importing the guide's anchors or
61
+ language selectors.
62
+ """
63
+ container = nodes.container()
64
+ if include_heading:
65
+ container += _rubric(title=section[0])
66
+ container += [child.deepcopy() for child in section.children[1:]]
67
+ for subsection in list(container.findall(condition=nodes.section)):
68
+ subsection.replace_self(new=[_rubric(title=subsection[0]), *subsection.children[1:]])
69
+ for target in list(container.findall(condition=nodes.target)):
70
+ assert target.parent is not None
71
+ target.parent.remove(item=target)
72
+ for element in container.findall(condition=nodes.Element):
73
+ element["ids"] = []
74
+ element["names"] = []
75
+ if isinstance(element, nodes.reference) and "refid" in element:
76
+ element["refuri"] = f"{guide}.html#{element['refid']}"
77
+ del element["refid"]
78
+ for collapse in list(container.findall(condition=CollapseNode)):
79
+ if collapse["label"] == language:
80
+ collapse.replace_self(
81
+ new=[child for child in collapse.children if not isinstance(child, CollapseSummaryNode)]
82
+ )
83
+ return container.children
84
+
85
+
86
+ class MiniMapSection(SphinxDirective):
87
+ """Include one parsed section from a maintained guide in one
88
+ language.
89
+ """
90
+
91
+ required_arguments = 1
92
+ option_spec: ClassVar[dict[str, Callable[[str], Any]] | None] = {
93
+ "section": directives.unchanged_required,
94
+ "language": directives.unchanged_required,
95
+ "include-heading": directives.flag,
96
+ }
97
+
98
+ def run(self) -> list[nodes.Node]:
99
+ """Select a guide section, preserving source files and dependency
100
+ tracking.
101
+ """
102
+ source_root = Path(self.env.srcdir).resolve()
103
+ _relative, filename = self.env.relfn2path(filename=self.arguments[0])
104
+ source_path = Path(filename).resolve()
105
+ if not source_path.is_relative_to(source_root) or source_path.suffix != ".rst":
106
+ raise self.error(message="Mini-map source must be an RST guide within the source directory")
107
+ if source_path == Path(self.env.doc2path(docname=self.env.docname)).resolve():
108
+ raise self.error(message="A mini-map cannot include itself")
109
+ if "section" not in self.options or "language" not in self.options:
110
+ raise self.error(message="mini-map-section requires :section: and :language:")
111
+ language = self.config.hackerrank_languages.get(self.options["language"])
112
+ if language is None:
113
+ raise self.error(message=f"Expected one mini-map language config for {self.options['language']!r}")
114
+ self.env.note_dependency(filename=source_path)
115
+ cache: dict[tuple[str, str], nodes.document] = self.state.document.attributes.setdefault("_mini_map_guides", {})
116
+ key = (str(object=source_path), self.options["language"])
117
+ if key not in cache:
118
+ settings = copy(x=self.state.document.settings)
119
+ document = new_document(
120
+ source_path=str(object=self.env.doc2path(docname=self.env.docname)), settings=settings
121
+ )
122
+ contexts = self.config.jinja2_contexts
123
+ context_name = self.config.hackerrank_jinja_context
124
+ self.config.jinja2_contexts = {
125
+ **contexts,
126
+ context_name: {**contexts.get(context_name, {}), **language.get("context", {})},
127
+ }
128
+ try:
129
+ source = "\n\n".join(
130
+ part
131
+ for part in (
132
+ self.config.rst_prolog or "",
133
+ f".. include:: /{source_path.relative_to(source_root).as_posix()}",
134
+ self.config.rst_epilog or "",
135
+ )
136
+ if part
137
+ )
138
+ Parser().parse(inputstring=source, document=document)
139
+ for transform in (
140
+ DefaultSubstitutions,
141
+ Substitutions,
142
+ PropagateTargets,
143
+ AnonymousHyperlinks,
144
+ IndirectHyperlinks,
145
+ ExternalTargets,
146
+ InternalTargets,
147
+ ):
148
+ transform(document=document).apply()
149
+ finally:
150
+ self.config.jinja2_contexts = contexts
151
+ cache[key] = document
152
+ try:
153
+ section = select_section(document=cache[key], selector=self.options["section"])
154
+ except ValueError as error:
155
+ raise self.error(message=str(object=error)) from error
156
+ content = section_content(
157
+ section=section,
158
+ guide=source_path.relative_to(source_root).with_suffix(suffix="").as_posix(),
159
+ language=language.get("name", self.options["language"]),
160
+ include_heading="include-heading" in self.options,
161
+ )
162
+ for root in content:
163
+ for node in root.findall():
164
+ node.document = self.state.document
165
+ return content
166
+
167
+
168
+ def discard_guide_cache(_app: Sphinx, doctree: nodes.document) -> None:
169
+ """Keep temporary language variants out of Sphinx's persisted doctrees."""
170
+ doctree.attributes.pop("_mini_map_guides", None)
171
+
172
+
173
+ def simplify_mini_map(_app: Sphinx, doctree: nodes.document, docname: str) -> None:
174
+ """Remove admonitions from mini-map views after includes render."""
175
+ if not _app.config.hackerrank_exclude_admonitions:
176
+ return
177
+ containers = [node for node in doctree.findall(condition=nodes.container) if "hr-mini-map" in node["classes"]]
178
+ for container in containers:
179
+ for admonition in list(container.findall(condition=lambda node: isinstance(node, nodes.Admonition))):
180
+ parent = admonition.parent
181
+ assert parent is not None
182
+ parent.remove(item=admonition)
@@ -0,0 +1,36 @@
1
+ .hr-mini-map {
2
+ font-family: Arial, sans-serif;
3
+ line-height: 1.5;
4
+ padding: 10px;
5
+ }
6
+ .hr-mini-map details {
7
+ margin-bottom: 12px;
8
+ }
9
+ .hr-mini-map summary {
10
+ cursor: pointer;
11
+ font-size: 16px;
12
+ font-weight: bold;
13
+ }
14
+ .hr-mini-map .rubric {
15
+ font-weight: bold;
16
+ margin-top: 1em;
17
+ }
18
+ .hr-mini-map pre {
19
+ background: #f5f5f5;
20
+ border: 1px solid #ddd;
21
+ color: #222;
22
+ overflow-x: auto;
23
+ padding: 10px;
24
+ white-space: pre;
25
+ }
26
+ .hr-mini-map table {
27
+ border-collapse: collapse;
28
+ width: 100%;
29
+ }
30
+ .hr-mini-map th,
31
+ .hr-mini-map td {
32
+ border: 1px solid #ddd;
33
+ padding: 8px;
34
+ text-align: left;
35
+ vertical-align: top;
36
+ }
@@ -0,0 +1,49 @@
1
+ """Read rendering metadata without depending on an API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Target:
14
+ """One language and its interviewer-facing title."""
15
+
16
+ language: str
17
+ name: str
18
+ metadata_path: Path
19
+
20
+
21
+ def load_targets(*, source_dir: Path, pattern: str) -> dict[str, list[Target]]:
22
+ """Read adjacent mapping files; publishing metadata stays with the consumer."""
23
+ targets: dict[str, list[Target]] = {}
24
+ for path in sorted(source_dir.glob(pattern)):
25
+ source = path.with_suffix(".rst")
26
+ if not source.is_file():
27
+ raise ValueError(f"Missing RST source beside {path}")
28
+ metadata = yaml.safe_load(path.read_text(encoding="utf-8"))
29
+ if not isinstance(metadata, dict) or metadata.get("version") != 1:
30
+ raise ValueError(f"Expected version: 1 in {path}")
31
+ questions = metadata.get("questions")
32
+ if not isinstance(questions, list) or not questions:
33
+ raise ValueError(f"Expected nonempty questions list in {path}")
34
+ document: list[Target] = []
35
+ seen: set[str] = set()
36
+ for question in questions:
37
+ if not isinstance(question, dict):
38
+ raise ValueError(f"Expected a question mapping in {path}")
39
+ language, name = question.get("language"), question.get("name")
40
+ if not isinstance(language, str) or re.fullmatch(r"[a-z][a-z0-9]*", language) is None:
41
+ raise ValueError(f"Invalid language in {path}: {language!r}")
42
+ if not isinstance(name, str) or not name.strip():
43
+ raise ValueError(f"Expected a question name in {path}")
44
+ if language in seen:
45
+ raise ValueError(f"Duplicate language {language!r} in {path}")
46
+ seen.add(language)
47
+ document.append(Target(language, name, path))
48
+ targets[source.relative_to(source_dir).with_suffix("").as_posix()] = document
49
+ return targets
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.5
2
+ Name: sphinx-hackerrank
3
+ Version: 2026.10.30.1
4
+ Summary: Build portable HackerRank interviewer mini-maps from Sphinx documentation
5
+ Project-URL: Source, https://github.com/adamtheturtle/sphinx-hackerrank
6
+ Project-URL: Issues, https://github.com/adamtheturtle/sphinx-hackerrank/issues
7
+ Author-email: Adam Dangoor <adamdangoor@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Sphinx :: Extension
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: beautifulsoup4>=4.12
18
+ Requires-Dist: pyyaml>=6
19
+ Requires-Dist: sphinx-jinja2>=0.0.1
20
+ Requires-Dist: sphinx-toolbox<5,>=4.3
21
+ Requires-Dist: sphinx<10,>=8.2
22
+ Description-Content-Type: text/x-rst
23
+
24
+ sphinx-hackerrank
25
+ =================
26
+
27
+ Build portable interviewer mini-maps from maintained Sphinx guides.
28
+ The ``hackerrank`` builder reuses Sphinx's HTML renderer and exports one
29
+ standalone HTML fragment per question and language, plus a local preview index.
30
+ No HackerRank login or API token is required to build or preview.
31
+
32
+ Installation
33
+ ------------
34
+
35
+ .. code-block:: console
36
+
37
+ pip install sphinx-hackerrank
38
+
39
+ Built wheels are also published as GitHub release assets:
40
+
41
+ .. code-block:: console
42
+
43
+ pip install https://github.com/adamtheturtle/sphinx-hackerrank/releases/download/2026.10.30.1/sphinx_hackerrank-2026.10.30.1-py3-none-any.whl
44
+
45
+ Enable the installed extension in ``conf.py``:
46
+
47
+ .. code-block:: python
48
+
49
+ extensions = ["sphinx_hackerrank"]
50
+
51
+ No ``sys.path`` changes are needed.
52
+
53
+ Authoring
54
+ ---------
55
+
56
+ Keep the full guide and its mini-map RST beside each other. Include native
57
+ sections by exact heading path or an existing RST section label:
58
+
59
+ .. code-block:: rst
60
+
61
+ .. container:: hr-mini-map mini-map-python
62
+
63
+ .. collapse:: Prompt
64
+
65
+ .. mini-map-section:: widgets.rst
66
+ :section: Part 1 / Prompt
67
+ :language: python
68
+
69
+ Use ``:section: #widget-prompt`` for a labelled section and
70
+ ``:include-heading:`` to retain the selected section's heading.
71
+ Nested headings become rubrics inside disclosures. Missing or ambiguous
72
+ selections are reported as directive errors; build with ``-W`` to reject them.
73
+ The guide does not need extraction markers or duplicated content.
74
+
75
+ The directive uses native includes, substitutions, and the guide's existing
76
+ Jinja templates. Variants are cached within each mini-map read and discarded
77
+ before Sphinx persists the document tree. Guide and included-file dependencies
78
+ are tracked for incremental builds.
79
+
80
+ Language contexts
81
+ -----------------
82
+
83
+ Configure how each language renders the guide's Jinja context:
84
+
85
+ .. code-block:: python
86
+
87
+ hackerrank_jinja_context = "snippets"
88
+ hackerrank_languages = {
89
+ "python": {
90
+ "name": "Python",
91
+ "context": {"variants": [{"name": "Python", "lexer": "python"}]},
92
+ },
93
+ }
94
+
95
+ ``context`` overrides values in the named Jinja context while that variant is
96
+ parsed. The original configuration is restored afterwards.
97
+ ``name`` identifies redundant language disclosures to flatten in copied content.
98
+ All other part, category, and test-alternative disclosures remain intact.
99
+
100
+ By default, admonitions are omitted within ``hr-mini-map`` containers.
101
+ Set ``hackerrank_exclude_admonitions = False`` to retain them.
102
+ The original guide's admonitions remain unchanged.
103
+
104
+ Question mappings
105
+ -----------------
106
+
107
+ Create a mapping beside each mini-map, such as ``widgets-mini-map.yml`` beside
108
+ ``widgets-mini-map.rst``:
109
+
110
+ .. code-block:: yaml
111
+
112
+ version: 1
113
+ questions:
114
+ - language: python
115
+ name: Widgets - Python
116
+
117
+ The default ``hackerrank_manifest_pattern`` is ``**/*-mini-map.yml``, relative
118
+ to Sphinx's source directory. Nested directories and custom source layouts are
119
+ supported. Extra fields such as question IDs can be managed by your publishing
120
+ tool; the builder reads only the language and name.
121
+
122
+ Build and preview
123
+ -----------------
124
+
125
+ .. code-block:: console
126
+
127
+ sphinx-build -W -b hackerrank docs build/hackerrank
128
+
129
+ Open ``build/hackerrank/mini-maps/index.html`` to select a question and language.
130
+ Each preview uses the same self-contained HTML that can be saved as HackerRank
131
+ interviewer notes. Native disclosure elements work without JavaScript.
132
+ The builder also produces ordinary themed HTML pages. The extension's directives
133
+ work in the standard ``html`` builder too.
134
+
135
+ Sphinx's native ``only`` directive can select ``builder_hackerrank``,
136
+ ``builder_html``, or the shared ``html`` format tag.
137
+
138
+ Source links
139
+ ------------
140
+
141
+ By default, links to full guide pages point back into the local HTML build.
142
+ To target a hosted documentation or source site, configure:
143
+
144
+ .. code-block:: python
145
+
146
+ hackerrank_source_url = "https://example.org/guides/"
147
+ hackerrank_source_suffix = ".html"
148
+ hackerrank_source_anchors = True
149
+
150
+ A source repository can use a different suffix, such as ``.rst``, and disable
151
+ anchors. External URLs and links within a fragment are retained.
152
+
153
+ Publishing
154
+ ----------
155
+
156
+ Publishing is separate from rendering. This package does not call the HackerRank
157
+ API or manage credentials, question identities, candidate prompts, or projects.
158
+ Your publishing tool can read the generated fragments and verify saved notes.
159
+
160
+ Development
161
+ -----------
162
+
163
+ .. code-block:: console
164
+
165
+ uv run pytest --cov --cov-report=term-missing
166
+ uv run ruff check .
167
+ uv run ruff format --check .
168
+ uv build
169
+ uv run python tests/wheel_smoke.py
170
+
171
+ The minimal example under ``examples/minimal/manual`` uses a custom Jinja context
172
+ and nested guide files. The wheel smoke test installs the built wheel into a
173
+ fresh environment and builds that example outside the checkout.
174
+
175
+ To release, update the version in ``pyproject.toml`` and
176
+ ``src/sphinx_hackerrank/__init__.py``, then publish a matching GitHub release.
177
+ The release workflow builds and checks distributions before uploading with a
178
+ PyPI trusted publisher configured for ``release.yml`` and environment ``release``.
179
+ Set the repository variable ``PYPI_PUBLISH_ENABLED`` to ``true`` after configuring
180
+ the publisher. The workflow can also be dispatched with a release ``tag``.
181
+
182
+ License
183
+ -------
184
+
185
+ MIT. See ``LICENSE``.
@@ -0,0 +1,11 @@
1
+ sphinx_hackerrank/__init__.py,sha256=XsYjow15EjBkJqZLZOwG_aJ8KyKvifbBa8grB_sbHZo,1482
2
+ sphinx_hackerrank/builder.py,sha256=penMdbLG6GUYS6GqVYx4wzkVJs7ybLR2RyO7vGwynDw,4698
3
+ sphinx_hackerrank/html.py,sha256=3tXAL65GkkXsOidB8pWPA4FdVFPDWPY2sgBNnkqMzSc,2246
4
+ sphinx_hackerrank/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ sphinx_hackerrank/sections.py,sha256=fhgIY3ZgaIUanBrauXnTONk__-kcimVEKFout48R180,7913
6
+ sphinx_hackerrank/targets.py,sha256=miSc8BtCa4IWt6Tb8Sxh92Iyp1XDXDueTnyH6CLG1mE,2044
7
+ sphinx_hackerrank/static/mini_map.css,sha256=i61odQc_pvKDCtVcHEEVs6UoP9q7I3qbysgx8UzN-ow,616
8
+ sphinx_hackerrank-2026.10.30.1.dist-info/METADATA,sha256=iaa9jzcTpwStOdE20oAB2vlZ-T2R9tLkErcOApLrczk,6296
9
+ sphinx_hackerrank-2026.10.30.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ sphinx_hackerrank-2026.10.30.1.dist-info/licenses/LICENSE,sha256=hvkADUCIndXpdC1L7yhnIKS99N0Wdtaf9YF06bRgZCo,1065
11
+ sphinx_hackerrank-2026.10.30.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rippling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.