diff --git a/.github/scripts/gen_ref_pages.py b/.github/scripts/gen_ref_pages.py new file mode 100644 index 0000000..0244f69 --- /dev/null +++ b/.github/scripts/gen_ref_pages.py @@ -0,0 +1,42 @@ +""" +Generate docs/reference/ files to disk. +This is roughly equivalent to what the `mkdocs-gen-files` plugin would have done. +""" + +from __future__ import annotations + +from pathlib import Path + +out_dir = Path("docs/reference") +out_dir.mkdir(parents=True, exist_ok=True) + +nav_lines = [] +seen_dirs: set[tuple[str, ...]] = set() + +for path in sorted(Path("src").rglob("*.py")): + module_path = path.relative_to("src").with_suffix("") + doc_path = path.relative_to("src").with_suffix(".md") + full_doc_path = out_dir / doc_path + + parts = tuple(module_path.parts) + ignore = ["_cli", "_version", "__init__", "__main__"] + if any(p in ignore for p in parts): + continue + + full_doc_path.parent.mkdir(parents=True, exist_ok=True) + full_doc_path.write_text(f"::: {'.'.join(parts)}\n") + + # Emit section headers for intermediate directories not yet seen + for i in range(1, len(parts)): + dir_key = parts[:i] + if dir_key not in seen_dirs: + seen_dirs.add(dir_key) + indent = " " * (len(dir_key) - 1) + nav_lines.append(f"{indent}* {dir_key[-1]}\n") + + # Emit leaf link + name = parts[-1] + indent = " " * (len(parts) - 1) + nav_lines.append(f"{indent}* [{name}]({doc_path.as_posix()})\n") + +(out_dir / "SUMMARY.md").write_text("".join(nav_lines)) diff --git a/.github/scripts/gen_zensical_toml.py b/.github/scripts/gen_zensical_toml.py new file mode 100644 index 0000000..03f3929 --- /dev/null +++ b/.github/scripts/gen_zensical_toml.py @@ -0,0 +1,107 @@ +""" " +Using _zensical.toml as a starting file, generate a zensical.toml with navigation links +generated from `docs/reference/SUMMARY.md`. + +This is roughly equivalent to what the `mkdocs-literate-nav` plugin would have done. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +def parse_summary(path: Path) -> list[tuple[int, str, str | None]]: + """Parse SUMMARY.md into a flat list of (indent_level, label, path_or_None).""" + items = [] + for line in path.read_text().splitlines(): + if not line.strip(): + continue + n_spaces = len(line) - len(line.lstrip(" ")) + indent = n_spaces // 4 + rest = line.strip() + if not rest.startswith("* "): + raise ValueError(f"Unexpected line format: {line!r}") + rest = rest[2:] + m = re.match(r"\[([^\]]+)\]\(([^)]+)\)", rest) + if m: + items.append((indent, m.group(1), m.group(2))) + else: + items.append((indent, rest, None)) + return items + + +def build_tree(items: list[tuple[int, str, str | None]]) -> list[dict]: + """Build a nested tree from flat (indent, label, path) items.""" + root: list[dict] = [] + # stack entries: (indent_level, children_list) + stack: list[tuple[int, list]] = [(-1, root)] + + for indent, label, path in items: + node: dict = {"label": label, "path": path, "children": []} + while stack[-1][0] >= indent: + stack.pop() + stack[-1][1].append(node) + stack.append((indent, node["children"])) + + return root + + +def render_children(children: list[dict], indent: int, path_prefix: str) -> list[str]: + """Render a list of nav nodes as indented TOML lines.""" + lines = [] + pad = " " * indent + for i, child in enumerate(children): + comma = "," if i < len(children) - 1 else "" + if not child["children"]: + path = path_prefix + child["path"] + lines.append(f'{pad}{{ "{child["label"]}" = "{path}" }}{comma}') + else: + lines.append(f'{pad}{{ "{child["label"]}" = [') + lines.extend(render_children(child["children"], indent + 1, path_prefix)) + lines.append(f"{pad}] }}{comma}") + return lines + + +def generate_code_docs_entry( + tree: list[dict], indent: int = 1, path_prefix: str = "reference/" +) -> str: + """Generate the full 'Code Documentation' nav entry as a TOML string.""" + pad = " " * indent + lines = [f'{pad}{{ "Code Documentation" = ['] + lines.extend(render_children(tree, indent + 1, path_prefix)) + lines.append(f"{pad}] }},") + return "\n".join(lines) + + +def main() -> None: + args = sys.argv[1:] + summary_path = Path(args[0] if args else "docs/reference/SUMMARY.md") + input_toml = Path(args[1] if len(args) > 1 else "_zensical.toml") + output_toml = Path(args[2] if len(args) > 2 else "zensical.toml") + + items = parse_summary(summary_path) + tree = build_tree(items) + code_docs_entry = generate_code_docs_entry(tree) + + toml_content = input_toml.read_text() + + placeholder_re = re.compile( + r'[ \t]*\{ "Code Documentation" = \[[\s\S]*?\] \},?[ \t]*\n' + ) + match = placeholder_re.search(toml_content) + if not match: + sys.exit(1) + + result = ( + toml_content[: match.start()] + + code_docs_entry + + "\n" + + toml_content[match.end() :] + ) + output_toml.write_text(result) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 9f67e21..99ecfc5 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -31,10 +31,19 @@ jobs: pip install uv uv pip install --system .[docs] + - name: Generate reference pages + run: python .github/scripts/gen_ref_pages.py + + - name: Generate zensical.toml + run: python .github/scripts/gen_zensical_toml.py + - name: Build docs - run: mkdocs build + run: zensical build --clean id: build_docs - - name: Rebuild and deploy docs - run: mkdocs gh-deploy --force + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 if: github.ref == 'refs/heads/main' && steps.build_docs.outcome == 'success' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site diff --git a/_zensical.toml b/_zensical.toml new file mode 100644 index 0000000..cb26859 --- /dev/null +++ b/_zensical.toml @@ -0,0 +1,163 @@ +[project] +site_name = "template" +site_description = "This is the template package!" +site_author = "YourName" +repo_url = "https://github.com/Quantum-Accelerators/template/" +edit_uri = "blob/main/docs/" +use_directory_urls = false + +nav = [ + { "Home" = "index.md" }, + { "Overview" = [ + "example_docs/intro/why.md", + "example_docs/intro/resources.md" + ] }, + { "Setup" = [ + "example_docs/setup/prep.md", + "example_docs/setup/name.md", + "example_docs/setup/basics.md" + ] }, + { "Installation" = [ + "example_docs/installation/pyproject.md", + "example_docs/installation/install.md" + ] }, + { "Code" = [ + "example_docs/code/source.md", + "example_docs/code/hints.md", + "example_docs/code/tests.md" + ] }, + { "Documentation" = [ + "example_docs/zensical/docs.md", + "example_docs/zensical/build.md" + ] }, + { "GitHub and CI" = [ + "example_docs/github/commits.md", + "example_docs/github/workflows.md" + ] }, + { "Code Documentation" = [ + "reference/" + ] }, + { "About" = [ + "example_docs/about/changelog.md", + "example_docs/about/conduct.md", + "example_docs/about/license.md" + ] } +] + +[project.theme] +features = [ + "content.action.edit", + "content.code.copy", + "content.code.select", + "content.code.annotate", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.path", + "navigation.tracking", + "navigation.sections", + "navigation.top", + "search.highlight", + "search.suggest", + "search.share", + "header.autohide", + "toc.follow" +] + +[project.theme.palette] +primary = "orange" +scheme = "slate" + +[project.markdown_extensions.abbr] + +[project.markdown_extensions.admonition] + +[project.markdown_extensions.attr_list] + +[project.markdown_extensions.def_list] + +[project.markdown_extensions.footnotes] + +[project.markdown_extensions.md_in_html] + +[project.markdown_extensions.toc] +permalink = true + +[project.markdown_extensions.pymdownx.arithmatex] +generic = true + +[project.markdown_extensions.pymdownx.betterem] +smart_enable = "all" + +[project.markdown_extensions.pymdownx.caret] + +[project.markdown_extensions.pymdownx.details] + +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true + +[project.markdown_extensions.pymdownx.inlinehilite] + +[project.markdown_extensions.pymdownx.keys] + +[project.markdown_extensions.pymdownx.magiclink] + +[project.markdown_extensions.pymdownx.mark] + +[project.markdown_extensions.pymdownx.snippets] + +[project.markdown_extensions.pymdownx.smartsymbols] + +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } +] + +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true + +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true + +[project.markdown_extensions.pymdownx.tilde] + +[project.plugins.search] +separator = '[\\s\\-,:!=\\[\\]()\"`/]+|\\.(?!\\d)|&[lg]t;|(?!\\b)(?=[A-Z][a-z])' + +[project.plugins.autorefs] + +# https://github.com/zensical/backlog/issues/37 +[project.plugins.social] + +[project.plugins.offline] + +[project.plugins.mkdocstrings] +default_handler = "python" + +[project.plugins.mkdocstrings.handlers.python] +inventories = [ + "https://docs.python.org/3/objects.inv", + "https://numpy.org/doc/stable/objects.inv" +] + +[project.plugins.mkdocstrings.handlers.python.options] +docstring_style = "numpy" +docstring_section_style = "list" +separate_signature = true +merge_init_into_class = true +show_signature_annotations = true +signature_crossrefs = true +show_if_no_docstring = true + +[project.plugins.mkdocstrings.handlers.python.options.docstring_options] +ignore_init_summary = true + +# https://github.com/zensical/backlog/issues/8 +[project.plugins.gen-files] +scripts = ["docs/gen_ref_pages.py"] + +# https://github.com/zensical/backlog/issues/13 +[project.plugins.literate-nav] +nav_file = "SUMMARY.md" diff --git a/docs/example_docs/mkdocs/build.md b/docs/example_docs/mkdocs/build.md deleted file mode 100644 index ae031af..0000000 --- a/docs/example_docs/mkdocs/build.md +++ /dev/null @@ -1,23 +0,0 @@ -# Building the Docs - -## The `mkdocs.yml` File - -Once you have added your documentation, you will need to update the `/mkdocs.yml` file with information about how you want to arrange the files. Specifically, you will need to update the `nav` secction of the `mkdocs.yml` file to point to all your individual `.md` files, organizing them by category. - -!!! Note - - Keep the `- Code Documentation: reference/` line in the `nav` section of `mkdocs.yml`. It will automatically transform your docstrings into beautiful documentation! The rest of the `nav` items you can replace. - -## The Build Process - -To see how your documentation will look in advance, you can build it locally by running the following command in the base directory: - -```bash -mkdocs serve -``` - -A URL will be printed out that you can open in your browser. - -## Deploying the Docs - -To allow your documentation to be visible via GitHub Pages, go to "Settings > Pages" in your repository's settings and make sure "Branch" is set to "gh-pages" instead of "main". diff --git a/docs/example_docs/zensical/build.md b/docs/example_docs/zensical/build.md new file mode 100644 index 0000000..20c8d5a --- /dev/null +++ b/docs/example_docs/zensical/build.md @@ -0,0 +1,29 @@ +# Building the Docs + +!!! Note + + This documentation refers to `zensical.toml` at several places. You may find that this repository has a `_zensical.toml` file instead. This is because Zensical is a very new project and still evolving. Before it matures into a fully-functional product, we'll be running a couple of scripts in the `.github/scripts` folder to convert this `_zensical.toml` to a final `zensical.toml` file. This extra manual step will go away soon (as will this note!). For the time being, you can assume that anything you read here in the context of `zensical.toml` applies to `_zensical.toml` interchangeably. + +## The `zensical.toml` File + +Once you have added your documentation, you will need to update the `/zensical.toml` file with information about how you want to arrange the files. Specifically, you will need to update the `nav` secction of the `zensical.toml` file to point to all your individual `.md` files, organizing them by category. + +!!! Note + + Keep the `- Code Documentation: reference/` line in the `nav` section of `zensical.toml`. It will automatically transform your docstrings into beautiful documentation! The rest of the `nav` items you can replace. + +## The Build Process + +To see how your documentation will look in advance, you can build it locally by running the following command in the base directory: + +```bash +python .github/scripts/gen_ref_pages.py +python .github/scripts/gen_zensical_toml.py +zensical serve +``` + +A URL will be printed out that you can open in your browser. + +## Deploying the Docs + +To allow your documentation to be visible via GitHub Pages, go to "Settings > Pages" in your repository's settings and make sure "Branch" is set to "gh-pages" instead of "main". diff --git a/docs/example_docs/mkdocs/docs.md b/docs/example_docs/zensical/docs.md similarity index 53% rename from docs/example_docs/mkdocs/docs.md rename to docs/example_docs/zensical/docs.md index 87ff3d2..0253a89 100644 --- a/docs/example_docs/mkdocs/docs.md +++ b/docs/example_docs/zensical/docs.md @@ -1,6 +1,6 @@ # Writing the Docs -## Mkdocs +## Zensical Now it's time to write some documentation! This isn't very difficult, and of course you're reading some documentation right now. The documentation is written using markdown, which is the same way GitHub comments are formatted. @@ -8,12 +8,12 @@ Now it's time to write some documentation! This isn't very difficult, and of cou Check out the [Markdown Guide](https://www.markdownguide.org/basic-syntax/) for an overview of the basic syntax. -This template repository uses a documentation format called mkdocs, specifically a useful theme called [Material for Mkdocs](https://squidfunk.github.io/mkdocs-material/). This enables many wonderful goodies like the "tip" callout you see above and much more. +This template repository uses a documentation format called [Zensical](https://zensical.org/docs/get-started/). This enables many wonderful goodies like the "tip" callout you see above and much more. ## Adding Markdown Files -Your documentation will live in the `/docs` folder. You can think of each markdown (`.md`) file as being a specific page in the documentation, and each folder as being a related collection of pages. The markdown page you are reading right now is found at `/docs/example_docs/mkdocs/docs.md`, for instance. Of course, you will want to replce the `/docs/example_docs` folder with your own documentation. +Your documentation will live in the `/docs` folder. You can think of each markdown (`.md`) file as being a specific page in the documentation, and each folder as being a related collection of pages. The markdown page you are reading right now is found at `/docs/example_docs/zensical/docs.md`, for instance. Of course, you will want to replce the `/docs/example_docs` folder with your own documentation. !!! Note - You typically do not need to touch the `/docs/gen_ref_pages.py` script. It is used to automatically build the documentation for your code from its docstrings. + You typically do not need to touch the `/.github/workflows/gen_ref_pages.py` script (or any other scripts in that folder). It is used to automatically build the documentation for your code from its docstrings. diff --git a/mkdocs.yml b/mkdocs.yml index 6650b55..a06f737 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -110,11 +110,11 @@ plugins: docstring_section_style: list separate_signature: true merge_init_into_class: true - docstring_options: - ignore_init_summary: true show_signature_annotations: true signature_crossrefs: true show_if_no_docstring: true + docstring_options: + ignore_init_summary: true - gen-files: scripts: - docs/gen_ref_pages.py diff --git a/pyproject.toml b/pyproject.toml index 351858b..46e465b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,8 @@ dependencies = ["numpy"] [project.optional-dependencies] dev = ["pytest>=7.4.0", "pytest-cov>=3.0.0", "ruff>=0.0.285"] docs = [ - "mkdocs-material>=9.4.0", "mkdocstrings-python>=2.0.0", - "mkdocs-gen-files>=0.5.0", - "mkdocs-literate-nav>=0.6.0", + "zensical>=0.0.24", "pillow>=10.0.0", "cairosvg>=2.7.1" ]