├── .codecov.yml ├── .cursor └── rules │ ├── avoid-debug-loops.mdc │ ├── dev-loop.mdc │ ├── git-commits.mdc │ └── notes-llms-txt.mdc ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ └── tests.yml ├── .gitignore ├── .python-version ├── .tmuxp.yaml ├── .tool-versions ├── .vim └── coc-settings.json ├── .windsurfrules ├── CHANGES ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Makefile ├── _static │ ├── css │ │ └── custom.css │ ├── django-docutils.css │ ├── favicon.ico │ └── img │ │ └── icons │ │ ├── android-chrome-144x144.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-256x256.png │ │ ├── android-chrome-36x36.png │ │ ├── android-chrome-384x384.png │ │ ├── android-chrome-48x48.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-72x72.png │ │ ├── android-chrome-96x96.png │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── logo-dark.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── ms-icon-70x70.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── _templates │ ├── layout.html │ ├── more.html │ ├── sidebar │ │ └── projects.html │ └── star.html ├── api │ ├── exc.md │ ├── index.md │ ├── lib │ │ ├── directives │ │ │ ├── code.md │ │ │ ├── index.md │ │ │ └── registry.md │ │ ├── index.md │ │ ├── metadata │ │ │ ├── extract.md │ │ │ ├── index.md │ │ │ ├── process.md │ │ │ └── processors.md │ │ ├── publisher.md │ │ ├── roles │ │ │ ├── common.md │ │ │ ├── email.md │ │ │ ├── file.md │ │ │ ├── github.md │ │ │ ├── hackernews.md │ │ │ ├── index.md │ │ │ ├── kbd.md │ │ │ ├── leanpub.md │ │ │ ├── pypi.md │ │ │ ├── readthedocs.md │ │ │ ├── registry.md │ │ │ ├── twitter.md │ │ │ ├── types.md │ │ │ ├── url.md │ │ │ └── wikipedia.md │ │ ├── settings.md │ │ ├── text.md │ │ ├── transforms │ │ │ ├── code.md │ │ │ ├── index.md │ │ │ └── toc.md │ │ ├── utils.md │ │ ├── views.md │ │ └── writers.md │ ├── template.md │ ├── templatetags │ │ ├── django_docutils.md │ │ └── index.md │ └── views.md ├── class_based_view.md ├── conf.py ├── faq.md ├── history.md ├── index.md ├── make.bat ├── manifest.json ├── quickstart.md ├── redirects.txt ├── template_filter.md └── template_tag.md ├── pyproject.toml ├── src └── django_docutils │ ├── __about__.py │ ├── __init__.py │ ├── _internal │ ├── __init__.py │ └── types.py │ ├── exc.py │ ├── lib │ ├── __init__.py │ ├── directives │ │ ├── __init__.py │ │ ├── code.py │ │ └── registry.py │ ├── metadata │ │ ├── __init__.py │ │ ├── extract.py │ │ ├── process.py │ │ ├── processors.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── test_extract.py │ │ │ └── test_process.py │ ├── publisher.py │ ├── roles │ │ ├── __init__.py │ │ ├── common.py │ │ ├── email.py │ │ ├── file.py │ │ ├── github.py │ │ ├── hackernews.py │ │ ├── kbd.py │ │ ├── leanpub.py │ │ ├── pypi.py │ │ ├── readthedocs.py │ │ ├── registry.py │ │ ├── twitter.py │ │ ├── types.py │ │ ├── url.py │ │ └── wikipedia.py │ ├── settings.py │ ├── templates │ │ └── rst │ │ │ ├── base.html │ │ │ └── raw.html │ ├── tests │ │ ├── __init__.py │ │ ├── test_utils.py │ │ └── test_writers.py │ ├── text.py │ ├── transforms │ │ ├── __init__.py │ │ ├── code.py │ │ └── toc.py │ ├── types.py │ ├── utils.py │ ├── views.py │ └── writers.py │ ├── py.typed │ ├── template.py │ ├── templatetags │ ├── __init__.py │ └── django_docutils.py │ └── views.py ├── tests ├── __init__.py ├── constants.py ├── rst_content │ └── home.rst ├── settings.py ├── templates │ └── base.html ├── test_docutils_roles.py ├── test_template.py └── test_templatetag.py └── uv.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: no 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | status: 10 | project: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | base: auto 15 | patch: off 16 | -------------------------------------------------------------------------------- /.cursor/rules/avoid-debug-loops.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: When stuck in debugging loops, break the cycle by minimizing to an MVP, removing debugging cruft, and documenting the issue completely for a fresh approach 3 | globs: *.py 4 | alwaysApply: true 5 | --- 6 | # Avoid Debug Loops 7 | 8 | When debugging becomes circular and unproductive, follow these steps: 9 | 10 | ## Detection 11 | - You have made multiple unsuccessful attempts to fix the same issue 12 | - You are adding increasingly complex code to address errors 13 | - Each fix creates new errors in a cascading pattern 14 | - You are uncertain about the root cause after 2-3 iterations 15 | 16 | ## Action Plan 17 | 18 | 1. **Pause and acknowledge the loop** 19 | - Explicitly state that you are in a potential debug loop 20 | - Review what approaches have been tried and failed 21 | 22 | 2. **Minimize to MVP** 23 | - Remove all debugging cruft and experimental code 24 | - Revert to the simplest version that demonstrates the issue 25 | - Focus on isolating the core problem without added complexity 26 | 27 | 3. **Comprehensive Documentation** 28 | - Provide a clear summary of the issue 29 | - Include minimal but complete code examples that reproduce the problem 30 | - Document exact error messages and unexpected behaviors 31 | - Explain your current understanding of potential causes 32 | 33 | 4. **Format for Portability** 34 | - Present the problem in quadruple backticks for easy copying: 35 | 36 | ```` 37 | # Problem Summary 38 | [Concise explanation of the issue] 39 | 40 | ## Minimal Reproduction Code 41 | ```python 42 | # Minimal code example that reproduces the issue 43 | ``` 44 | 45 | ## Error/Unexpected Output 46 | ``` 47 | [Exact error messages or unexpected output] 48 | ``` 49 | 50 | ## Failed Approaches 51 | [Brief summary of approaches already tried] 52 | 53 | ## Suspected Cause 54 | [Your current hypothesis about what might be causing the issue] 55 | ```` 56 | 57 | This format enables the user to easily copy the entire problem statement into a fresh conversation for a clean-slate approach. 58 | -------------------------------------------------------------------------------- /.cursor/rules/dev-loop.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: QA every edit 3 | globs: *.py 4 | alwaysApply: true 5 | --- 6 | 7 | # Development Process 8 | 9 | ## Project Stack 10 | 11 | The project uses the following tools and technologies: 12 | 13 | - **uv** - Python package management and virtual environments 14 | - **ruff** - Fast Python linter and formatter 15 | - **py.test** - Testing framework 16 | - **pytest-watcher** - Continuous test runner 17 | - **mypy** - Static type checking 18 | - **doctest** - Testing code examples in documentation 19 | 20 | ## 1. Start with Formatting 21 | 22 | Format your code first: 23 | 24 | ``` 25 | uv run ruff format . 26 | ``` 27 | 28 | ## 2. Run Tests 29 | 30 | Verify that your changes pass the tests: 31 | 32 | ``` 33 | uv run py.test 34 | ``` 35 | 36 | For continuous testing during development, use pytest-watcher: 37 | 38 | ``` 39 | # Watch all tests 40 | uv run ptw . 41 | 42 | # Watch and run tests immediately, including doctests 43 | uv run ptw . --now --doctest-modules 44 | 45 | # Watch specific files or directories 46 | uv run ptw . --now --doctest-modules src/libtmux/_internal/ 47 | ``` 48 | 49 | ## 3. Commit Initial Changes 50 | 51 | Make an atomic commit for your changes using conventional commits. 52 | Use `@git-commits.mdc` for assistance with commit message standards. 53 | 54 | ## 4. Run Linting and Type Checking 55 | 56 | Check and fix linting issues: 57 | 58 | ``` 59 | uv run ruff check . --fix --show-fixes 60 | ``` 61 | 62 | Check typings: 63 | 64 | ``` 65 | uv run mypy 66 | ``` 67 | 68 | ## 5. Verify Tests Again 69 | 70 | Ensure tests still pass after linting and type fixes: 71 | 72 | ``` 73 | uv run py.test 74 | ``` 75 | 76 | ## 6. Final Commit 77 | 78 | Make a final commit with any linting/typing fixes. 79 | Use `@git-commits.mdc` for assistance with commit message standards. 80 | 81 | ## Development Loop Guidelines 82 | 83 | If there are any failures at any step due to your edits, fix them before proceeding to the next step. 84 | 85 | ## Python Code Standards 86 | 87 | ### Docstring Guidelines 88 | 89 | For `src/**/*.py` files, follow these docstring guidelines: 90 | 91 | 1. **Use reStructuredText format** for all docstrings. 92 | ```python 93 | """Short description of the function or class. 94 | 95 | Detailed description using reStructuredText format. 96 | 97 | Parameters 98 | ---------- 99 | param1 : type 100 | Description of param1 101 | param2 : type 102 | Description of param2 103 | 104 | Returns 105 | ------- 106 | type 107 | Description of return value 108 | """ 109 | ``` 110 | 111 | 2. **Keep the main description on the first line** after the opening `"""`. 112 | 113 | 3. **Use NumPy docstyle** for parameter and return value documentation. 114 | 115 | ### Doctest Guidelines 116 | 117 | For doctests in `src/**/*.py` files: 118 | 119 | 1. **Use narrative descriptions** for test sections rather than inline comments: 120 | ```python 121 | """Example function. 122 | 123 | Examples 124 | -------- 125 | Create an instance: 126 | 127 | >>> obj = ExampleClass() 128 | 129 | Verify a property: 130 | 131 | >>> obj.property 132 | 'expected value' 133 | """ 134 | ``` 135 | 136 | 2. **Move complex examples** to dedicated test files at `tests/examples//test_.py` if they require elaborate setup or multiple steps. 137 | 138 | 3. **Utilize pytest fixtures** via `doctest_namespace` for more complex test scenarios: 139 | ```python 140 | """Example with fixture. 141 | 142 | Examples 143 | -------- 144 | >>> # doctest_namespace contains all pytest fixtures from conftest.py 145 | >>> example_fixture = getfixture('example_fixture') 146 | >>> example_fixture.method() 147 | 'expected result' 148 | """ 149 | ``` 150 | 151 | 4. **Keep doctests simple and focused** on demonstrating usage rather than comprehensive testing. 152 | 153 | 5. **Add blank lines between test sections** for improved readability. 154 | 155 | 6. **Test your doctests continuously** using pytest-watcher during development: 156 | ``` 157 | # Watch specific modules for doctest changes 158 | uv run ptw . --now --doctest-modules src/path/to/module.py 159 | ``` 160 | 161 | ### Pytest Testing Guidelines 162 | 163 | 1. **Use existing fixtures over mocks**: 164 | - Use fixtures from conftest.py instead of `monkeypatch` and `MagicMock` when available 165 | - For instance, if using libtmux, use provided fixtures: `server`, `session`, `window`, and `pane` 166 | - Document in test docstrings why standard fixtures weren't used for exceptional cases 167 | 168 | 2. **Preferred pytest patterns**: 169 | - Use `tmp_path` (pathlib.Path) fixture over Python's `tempfile` 170 | - Use `monkeypatch` fixture over `unittest.mock` 171 | 172 | ### Import Guidelines 173 | 174 | 1. **Prefer namespace imports**: 175 | - Import modules and access attributes through the namespace instead of importing specific symbols 176 | - Example: Use `import enum` and access `enum.Enum` instead of `from enum import Enum` 177 | - This applies to standard library modules like `pathlib`, `os`, and similar cases 178 | 179 | 2. **Standard aliases**: 180 | - For `typing` module, use `import typing as t` 181 | - Access typing elements via the namespace: `t.NamedTuple`, `t.TypedDict`, etc. 182 | - Note primitive types like unions can be done via `|` pipes and primitive types like list and dict can be done via `list` and `dict` directly. 183 | 184 | 3. **Benefits of namespace imports**: 185 | - Improves code readability by making the source of symbols clear 186 | - Reduces potential naming conflicts 187 | - Makes import statements more maintainable 188 | -------------------------------------------------------------------------------- /.cursor/rules/git-commits.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: git-commits: Git commit message standards and AI assistance 3 | globs: git-commits: Git commit message standards and AI assistance | *.git/* .gitignore .github/* CHANGELOG.md CHANGES.md 4 | alwaysApply: true 5 | --- 6 | # Optimized Git Commit Standards 7 | 8 | ## Commit Message Format 9 | ``` 10 | Component/File(commit-type[Subcomponent/method]): Concise description 11 | 12 | why: Explanation of necessity or impact. 13 | what: 14 | - Specific technical changes made 15 | - Focused on a single topic 16 | 17 | refs: #issue-number, breaking changes, or relevant links 18 | ``` 19 | 20 | ## Component Patterns 21 | ### General Code Changes 22 | ``` 23 | Component/File(feat[method]): Add feature 24 | Component/File(fix[method]): Fix bug 25 | Component/File(refactor[method]): Code restructure 26 | ``` 27 | 28 | ### Packages and Dependencies 29 | | Language | Standard Packages | Dev Packages | Extras / Sub-packages | 30 | |------------|------------------------------------|-------------------------------|-----------------------------------------------| 31 | | General | `lang(deps):` | `lang(deps[dev]):` | | 32 | | Python | `py(deps):` | `py(deps[dev]):` | `py(deps[extra]):` | 33 | | JavaScript | `js(deps):` | `js(deps[dev]):` | `js(deps[subpackage]):`, `js(deps[dev{subpackage}]):` | 34 | 35 | #### Examples 36 | - `py(deps[dev]): Update pytest to v8.1` 37 | - `js(deps[ui-components]): Upgrade Button component package` 38 | - `js(deps[dev{linting}]): Add ESLint plugin` 39 | 40 | ### Documentation Changes 41 | Prefix with `docs:` 42 | ``` 43 | docs(Component/File[Subcomponent/method]): Update API usage guide 44 | ``` 45 | 46 | ### Test Changes 47 | Prefix with `tests:` 48 | ``` 49 | tests(Component/File[Subcomponent/method]): Add edge case tests 50 | ``` 51 | 52 | ## Commit Types Summary 53 | - **feat**: New features or enhancements 54 | - **fix**: Bug fixes 55 | - **refactor**: Code restructuring without functional change 56 | - **docs**: Documentation updates 57 | - **chore**: Maintenance (dependencies, tooling, config) 58 | - **test**: Test-related updates 59 | - **style**: Code style and formatting 60 | 61 | ## General Guidelines 62 | - Subject line: Maximum 50 characters 63 | - Body lines: Maximum 72 characters 64 | - Use imperative mood (e.g., "Add", "Fix", not "Added", "Fixed") 65 | - Limit to one topic per commit 66 | - Separate subject from body with a blank line 67 | - Mark breaking changes clearly: `BREAKING:` 68 | - Use `See also:` to provide external references 69 | 70 | ## AI Assistance Workflow in Cursor 71 | - Stage changes with `git add` 72 | - Use `@commit` to generate initial commit message 73 | - Review and refine generated message 74 | - Ensure adherence to these standards 75 | 76 | ## Good Commit Example 77 | ``` 78 | Pane(feat[capture_pane]): Add screenshot capture support 79 | 80 | why: Provide visual debugging capability 81 | what: 82 | - Implement capturePane method with image export 83 | - Integrate with existing Pane component logic 84 | - Document usage in Pane README 85 | 86 | refs: #485 87 | See also: https://example.com/docs/pane-capture 88 | ``` 89 | 90 | ## Bad Commit Example 91 | ``` 92 | fixed stuff and improved some functions 93 | ``` 94 | 95 | These guidelines ensure clear, consistent commit histories, facilitating easier code review and maintenance. -------------------------------------------------------------------------------- /.cursor/rules/notes-llms-txt.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: LLM-friendly markdown format for notes directories 3 | globs: notes/**/*.md,**/notes/**/*.md 4 | alwaysApply: true 5 | --- 6 | 7 | # Instructions for Generating LLM-Optimized Markdown Content 8 | 9 | When creating or editing markdown files within the specified directories, adhere to the following guidelines to ensure the content is optimized for LLM understanding and efficient token usage: 10 | 11 | 1. **Conciseness and Clarity**: 12 | - **Be Brief**: Present information succinctly, avoiding unnecessary elaboration. 13 | - **Use Clear Language**: Employ straightforward language to convey ideas effectively. 14 | 15 | 2. **Structured Formatting**: 16 | - **Headings**: Utilize markdown headings (`#`, `##`, `###`, etc.) to organize content hierarchically. 17 | - **Lists**: Use bullet points (`-`) or numbered lists (`1.`, `2.`, etc.) to enumerate items clearly. 18 | - **Code Blocks**: Enclose code snippets within triple backticks (```) to distinguish them from regular text. 19 | 20 | 3. **Semantic Elements**: 21 | - **Emphasis**: Use asterisks (`*`) or underscores (`_`) for italicizing text to denote emphasis. 22 | - **Strong Emphasis**: Use double asterisks (`**`) or double underscores (`__`) for bold text to highlight critical points. 23 | - **Inline Code**: Use single backticks (`) for inline code references. 24 | 25 | 4. **Linking and References**: 26 | - **Hyperlinks**: Format links using `[Link Text](mdc:URL)` to provide direct access to external resources. 27 | - **References**: When citing sources, use footnotes or inline citations to maintain readability. 28 | 29 | 5. **Avoid Redundancy**: 30 | - **Eliminate Repetition**: Ensure that information is not unnecessarily repeated within the document. 31 | - **Use Summaries**: Provide brief summaries where detailed explanations are not essential. 32 | 33 | 6. **Standard Compliance**: 34 | - **llms.txt Conformance**: Structure the document in alignment with the `llms.txt` standard, which includes: 35 | - An H1 heading with the project or site name. 36 | - A blockquote summarizing the project's purpose. 37 | - Additional markdown sections providing detailed information. 38 | - H2-delimited sections containing lists of URLs for further details. 39 | 40 | By following these guidelines, the markdown files will be tailored for optimal LLM processing, ensuring that the content is both accessible and efficiently tokenized for AI applications. 41 | 42 | For more information on the `llms.txt` standard, refer to the official documentation: https://llmstxt.org/ 43 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | # Check for updates to GitHub Actions every week 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ['3.13'] 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Filter changed file paths to outputs 18 | uses: dorny/paths-filter@v3.0.2 19 | id: changes 20 | with: 21 | filters: | 22 | root_docs: 23 | - CHANGES 24 | - README.* 25 | docs: 26 | - 'docs/**' 27 | - 'examples/**' 28 | python_files: 29 | - 'src/django_docutils/**' 30 | - pyproject.toml 31 | - uv.lock 32 | 33 | - name: Should publish 34 | if: steps.changes.outputs.docs == 'true' || steps.changes.outputs.root_docs == 'true' || steps.changes.outputs.python_files == 'true' 35 | run: echo "PUBLISH=$(echo true)" >> $GITHUB_ENV 36 | 37 | - name: Install uv 38 | if: env.PUBLISH == 'true' 39 | uses: astral-sh/setup-uv@v5 40 | with: 41 | enable-cache: true 42 | 43 | - name: Set up Python ${{ matrix.python-version }} 44 | if: env.PUBLISH == 'true' 45 | run: uv python install ${{ matrix.python-version }} 46 | 47 | - name: Install dependencies [w/ docs] 48 | if: env.PUBLISH == 'true' 49 | run: uv sync --all-extras --dev 50 | 51 | - name: Print python versions 52 | if: env.PUBLISH == 'true' 53 | run: | 54 | python -V 55 | uv run python -V 56 | 57 | - name: Build documentation 58 | if: env.PUBLISH == 'true' 59 | run: | 60 | pushd docs; make SPHINXBUILD='uv run sphinx-build' html; popd 61 | 62 | - name: Push documentation to S3 63 | if: env.PUBLISH == 'true' 64 | uses: jakejarvis/s3-sync-action@v0.5.1 65 | with: 66 | args: --acl public-read --follow-symlinks --delete 67 | env: 68 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 69 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 70 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 71 | AWS_REGION: 'us-west-1' # optional: defaults to us-east-1 72 | SOURCE_DIR: 'docs/_build/html' # optional: defaults to entire repository 73 | 74 | - name: Purge cache on Cloudflare 75 | if: env.PUBLISH == 'true' 76 | uses: jakejarvis/cloudflare-purge-action@v0.3.0 77 | env: 78 | CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }} 79 | CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }} 80 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ['3.10', '3.13'] 11 | django-version: ['4.2', '5.0', '5.1'] 12 | include: 13 | - python-version: '3.9' 14 | django-version: '4.2' 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install uv 19 | uses: astral-sh/setup-uv@v5 20 | with: 21 | enable-cache: true 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | run: uv python install ${{ matrix.python-version }} 25 | 26 | - name: Install dependencies 27 | run: uv sync --all-extras --dev 28 | 29 | - name: Install django @ ${{ matrix.django-version }} 30 | run: uv pip install DJANGO~=${{ matrix.django-version }} 31 | 32 | - name: Print python versions 33 | run: | 34 | python -V 35 | uv run python -V 36 | 37 | - name: Lint with ruff check 38 | run: uv run ruff check . 39 | 40 | - name: Format with ruff format 41 | run: uv run ruff format . --check 42 | 43 | - name: Lint with mypy 44 | run: uv run mypy . 45 | 46 | - name: Test with pytest 47 | run: uv run py.test --cov=./ --cov-report=xml 48 | 49 | - uses: codecov/codecov-action@v5 50 | with: 51 | token: ${{ secrets.CODECOV_TOKEN }} 52 | 53 | release: 54 | runs-on: ubuntu-latest 55 | needs: build 56 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 57 | 58 | strategy: 59 | matrix: 60 | python-version: ['3.13'] 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | 65 | - name: Install uv 66 | uses: astral-sh/setup-uv@v5 67 | with: 68 | enable-cache: true 69 | 70 | - name: Set up Python ${{ matrix.python-version }} 71 | run: uv python install ${{ matrix.python-version }} 72 | 73 | - name: Build package 74 | run: uv build 75 | 76 | 77 | - name: Publish package 78 | uses: pypa/gh-action-pypi-publish@release/v1 79 | with: 80 | user: __token__ 81 | password: ${{ secrets.PYPI_API_TOKEN }} 82 | skip_existing: true 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | pip-wheel-metadata 18 | .installed.cfg 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Complexity 38 | output/*.html 39 | output/*/index.html 40 | 41 | # Sphinx 42 | docs/_build 43 | 44 | .eggs/ 45 | .cache/ 46 | .mypy_cache/ 47 | .pytest_cache/ 48 | .*env*/ 49 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13.0 2 | -------------------------------------------------------------------------------- /.tmuxp.yaml: -------------------------------------------------------------------------------- 1 | session_name: django-docutils 2 | start_directory: ./ # load session relative to config location (project root). 3 | windows: 4 | - window_name: django-docutils 5 | focus: True 6 | layout: main-horizontal 7 | options: 8 | main-pane-height: 67% 9 | panes: 10 | - focus: true 11 | - pane 12 | - make watch_test 13 | - window_name: docs 14 | layout: main-horizontal 15 | options: 16 | main-pane-height: 67% 17 | start_directory: docs/ 18 | panes: 19 | - focus: true 20 | - pane 21 | - pane 22 | - make start 23 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | uv 0.7.20 2 | python 3.13.5 3.12.11 3.11.13 3.10.18 3.9.23 3 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[markdown][python]": { 3 | "coc.preferences.formatOnSave": true 4 | }, 5 | "python.analysis.autoSearchPaths": true, 6 | "python.analysis.typeCheckingMode": "basic", 7 | "python.analysis.useLibraryCodeForTypes": true, 8 | "python.formatting.provider": "ruff", 9 | "python.linting.ruffEnabled": true, 10 | "python.linting.mypyEnabled": true, 11 | "python.linting.flake8Enabled": false, 12 | "python.linting.pyflakesEnabled": false, 13 | "python.linting.pycodestyleEnabled": false, 14 | "python.linting.banditEnabled": false, 15 | "python.linting.pylamaEnabled": false, 16 | "python.linting.pylintEnabled": false, 17 | "pyright.organizeimports.provider": "ruff", 18 | "pyright.testing.provider": "pytest", 19 | } 20 | -------------------------------------------------------------------------------- /.windsurfrules: -------------------------------------------------------------------------------- 1 | # libtmux Python Project Rules 2 | 3 | 4 | - uv - Python package management and virtual environments 5 | - ruff - Fast Python linter and formatter 6 | - py.test - Testing framework 7 | - pytest-watcher - Continuous test runner 8 | - mypy - Static type checking 9 | - doctest - Testing code examples in documentation 10 | 11 | 12 | 13 | - Use a consistent coding style throughout the project 14 | - Format code with ruff before committing 15 | - Run linting and type checking before finalizing changes 16 | - Verify tests pass after each significant change 17 | 18 | 19 | 20 | - Use reStructuredText format for all docstrings in src/**/*.py files 21 | - Keep the main description on the first line after the opening `"""` 22 | - Use NumPy docstyle for parameter and return value documentation 23 | - Format docstrings as follows: 24 | ```python 25 | """Short description of the function or class. 26 | 27 | Detailed description using reStructuredText format. 28 | 29 | Parameters 30 | ---------- 31 | param1 : type 32 | Description of param1 33 | param2 : type 34 | Description of param2 35 | 36 | Returns 37 | ------- 38 | type 39 | Description of return value 40 | """ 41 | ``` 42 | 43 | 44 | 45 | - Use narrative descriptions for test sections rather than inline comments 46 | - Format doctests as follows: 47 | ```python 48 | """ 49 | Examples 50 | -------- 51 | Create an instance: 52 | 53 | >>> obj = ExampleClass() 54 | 55 | Verify a property: 56 | 57 | >>> obj.property 58 | 'expected value' 59 | """ 60 | ``` 61 | - Add blank lines between test sections for improved readability 62 | - Keep doctests simple and focused on demonstrating usage 63 | - Move complex examples to dedicated test files at tests/examples//test_.py 64 | - Utilize pytest fixtures via doctest_namespace for complex scenarios 65 | 66 | 67 | 68 | - Run tests with `uv run py.test` before committing changes 69 | - Use pytest-watcher for continuous testing: `uv run ptw . --now --doctest-modules` 70 | - Fix any test failures before proceeding with additional changes 71 | 72 | 73 | 74 | - Make atomic commits with conventional commit messages 75 | - Start with an initial commit of functional changes 76 | - Follow with separate commits for formatting, linting, and type checking fixes 77 | 78 | 79 | 80 | - Use the following commit message format: 81 | ``` 82 | Component/File(commit-type[Subcomponent/method]): Concise description 83 | 84 | why: Explanation of necessity or impact. 85 | what: 86 | - Specific technical changes made 87 | - Focused on a single topic 88 | 89 | refs: #issue-number, breaking changes, or relevant links 90 | ``` 91 | 92 | - Common commit types: 93 | - **feat**: New features or enhancements 94 | - **fix**: Bug fixes 95 | - **refactor**: Code restructuring without functional change 96 | - **docs**: Documentation updates 97 | - **chore**: Maintenance (dependencies, tooling, config) 98 | - **test**: Test-related updates 99 | - **style**: Code style and formatting 100 | 101 | - Prefix Python package changes with: 102 | - `py(deps):` for standard packages 103 | - `py(deps[dev]):` for development packages 104 | - `py(deps[extra]):` for extras/sub-packages 105 | 106 | - General guidelines: 107 | - Subject line: Maximum 50 characters 108 | - Body lines: Maximum 72 characters 109 | - Use imperative mood (e.g., "Add", "Fix", not "Added", "Fixed") 110 | - Limit to one topic per commit 111 | - Separate subject from body with a blank line 112 | - Mark breaking changes clearly: `BREAKING:` 113 | 114 | 115 | 116 | - Use fixtures from conftest.py instead of monkeypatch and MagicMock when available 117 | - For instance, if using libtmux, use provided fixtures: server, session, window, and pane 118 | - Document in test docstrings why standard fixtures weren't used for exceptional cases 119 | - Use tmp_path (pathlib.Path) fixture over Python's tempfile 120 | - Use monkeypatch fixture over unittest.mock 121 | 122 | 123 | 124 | - Prefer namespace imports over importing specific symbols 125 | - Import modules and access attributes through the namespace: 126 | - Use `import enum` and access `enum.Enum` instead of `from enum import Enum` 127 | - This applies to standard library modules like pathlib, os, and similar cases 128 | - For typing, use `import typing as t` and access via the namespace: 129 | - Access typing elements as `t.NamedTuple`, `t.TypedDict`, etc. 130 | - Note primitive types like unions can be done via `|` pipes 131 | - Primitive types like list and dict can be done via `list` and `dict` directly 132 | - Benefits of namespace imports: 133 | - Improves code readability by making the source of symbols clear 134 | - Reduces potential naming conflicts 135 | - Makes import statements more maintainable 136 | 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015- tmuxp contributors 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PY_FILES= find . -type f -not -path '*/\.*' | grep -i '.*[.]py$$' 2> /dev/null 2 | DOC_FILES= find . -type f -not -path '*/\.*' | grep -i '.*[.]rst\$\|.*[.]md\$\|.*[.]css\$\|.*[.]py\$\|mkdocs\.yml\|CHANGES\|TODO\|.*conf\.py' 2> /dev/null 3 | SHELL := /bin/bash 4 | 5 | 6 | entr_warn: 7 | @echo "----------------------------------------------------------" 8 | @echo " ! File watching functionality non-operational ! " 9 | @echo " " 10 | @echo "Install entr(1) to automatically run tasks on file change." 11 | @echo "See https://eradman.com/entrproject/ " 12 | @echo "----------------------------------------------------------" 13 | 14 | test: 15 | uv run py.test $(test) 16 | 17 | start: 18 | $(MAKE) test; uv run ptw . 19 | 20 | watch_test: 21 | if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) test; else $(MAKE) test entr_warn; fi 22 | 23 | build_docs: 24 | $(MAKE) -C docs html 25 | 26 | start_docs: 27 | $(MAKE) -C docs start 28 | 29 | design_docs: 30 | $(MAKE) -C docs design 31 | 32 | ruff_format: 33 | uv run ruff format . 34 | 35 | ruff: 36 | ruff check . 37 | 38 | watch_ruff: 39 | if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) ruff; else $(MAKE) ruff entr_warn; fi 40 | 41 | mypy: 42 | uv run mypy `${PY_FILES}` 43 | 44 | watch_mypy: 45 | if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) mypy; else $(MAKE) mypy entr_warn; fi 46 | 47 | format_markdown: 48 | prettier --parser=markdown -w *.md docs/*.md docs/**/*.md CHANGES 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-docutils · [![Python Package](https://img.shields.io/pypi/v/django-docutils.svg)](https://pypi.org/project/django-docutils/) [![License](https://img.shields.io/github/license/tony/django-docutils.svg)](https://github.com/tony/django-docutils/blob/master/LICENSE) 2 | 3 | docutils (a.k.a. reStructuredText / rst / reST) support for Django. 4 | 5 | ## Quickstart 6 | 7 | Install django-docutils: 8 | 9 | ```console 10 | $ pip install django-docutils 11 | ``` 12 | 13 | Next, add `django_docutils` to your `INSTALLED_APPS` in your settings file: 14 | 15 | ```python 16 | INSTALLED_APPS = [ 17 | # ... your default apps, 18 | 'django_docutils' 19 | ] 20 | ``` 21 | 22 | ## Template tag 23 | 24 | In your template: 25 | 26 | ```django 27 | {% load django_docutils %} 28 | {% rst %} 29 | # hey 30 | # how's it going 31 | A. hows 32 | B. it 33 | 34 | C. going 35 | D. today 36 | 37 | **hi** 38 | *hi* 39 | {% endrst %} 40 | ``` 41 | 42 | ## Template filter 43 | 44 | In your template: 45 | 46 | ```django 47 | {% load django_docutils %} 48 | {% filter rst %} 49 | # hey 50 | # how's it going 51 | A. hows 52 | B. it 53 | 54 | C. going 55 | D. today 56 | 57 | **hi** 58 | *hi* 59 | {% endfilter %} 60 | ``` 61 | 62 | ## Template engine (class-based view) 63 | 64 | You can also use a class-based view to render reStructuredText (reST). 65 | 66 | If you want to use reStructuredText as a django template engine, `INSTALLED_APPS` _isn't_ required, 67 | instead you add this to your `TEMPLATES` variable in your settings: 68 | 69 | ```python 70 | TEMPLATES = [ 71 | # ... Other engines 72 | { 73 | "NAME": "docutils", 74 | "BACKEND": "django_docutils.template.DocutilsTemplates", 75 | "DIRS": [], 76 | "APP_DIRS": True, 77 | } 78 | ] 79 | ``` 80 | 81 | Now django will be able to scan for .rst files and process them. In your view: 82 | 83 | ```python 84 | from django_docutils.views import DocutilsView 85 | 86 | class HomeView(DocutilsView): 87 | template_name = 'base.html' 88 | rst_name = 'home.rst' 89 | ``` 90 | 91 | # Settings 92 | 93 | ```python 94 | # Optional, automatically maps roles, directives and transformers 95 | DJANGO_DOCUTILS_LIB_RST = { 96 | "docutils": { 97 | "raw_enabled": True, 98 | "strip_comments": True, 99 | "initial_header_level": 2, 100 | }, 101 | "roles": { 102 | "local": { 103 | "gh": "django_docutils.lib.roles.github.github_role", 104 | "twitter": "django_docutils.lib.roles.twitter.twitter_role", 105 | "email": "django_docutils.lib.roles.email.email_role", 106 | } 107 | }, 108 | "directives": { 109 | "code-block": "django_docutils.lib.directives.code.CodeBlock", 110 | } 111 | } 112 | 113 | # Optional 114 | DJANGO_DOCUTILS_LIB_TEXT = { 115 | "uncapitalized_word_filters": ["project.my_module.my_capitalization_fn"] 116 | } 117 | ``` 118 | 119 | ## More information 120 | 121 | - Python 3.9+ 122 | - Django 4.2+ 123 | 124 | [![Docs](https://github.com/tony/django-docutils/workflows/docs/badge.svg)](https://github.com/tony/django-docutils/actions?query=workflow%3A%22Docs%22) 125 | [![Build Status](https://github.com/tony/django-docutils/workflows/tests/badge.svg)](https://github.com/tony/django-docutils/actions?query=workflow%3A%22tests%22) 126 | [![Code Coverage](https://codecov.io/gh/tony/django-docutils/branch/master/graph/badge.svg)](https://codecov.io/gh/tony/django-docutils) 127 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .sidebar-tree p.indented-block { 2 | padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0 3 | var(--sidebar-item-spacing-horizontal); 4 | margin-bottom: 0; 5 | } 6 | 7 | .sidebar-tree p.indented-block span.indent { 8 | margin-left: var(--sidebar-item-spacing-horizontal); 9 | display: block; 10 | } 11 | 12 | .sidebar-tree p.indented-block .project-name { 13 | font-size: var(--sidebar-item-font-size); 14 | font-weight: bold; 15 | margin-right: calc(var(--sidebar-item-spacing-horizontal) / 2.5); 16 | } 17 | 18 | .sidebar-tree .active { 19 | font-weight: bold; 20 | } 21 | -------------------------------------------------------------------------------- /docs/_static/django-docutils.css: -------------------------------------------------------------------------------- 1 | h1.logo { 2 | font-size: 20px; 3 | } 4 | 5 | div.sidebar { 6 | margin: 0px; 7 | border: 0px; 8 | padding: 0px; 9 | background-color: inherit; 10 | } 11 | 12 | div.sidebar .sidebar-title { 13 | display: none; 14 | } 15 | 16 | form.navbar-form { 17 | padding: 0px 10px; 18 | } 19 | 20 | div#changelog > div.section > ul > li > p:only-child { 21 | margin-bottom: 0; 22 | } 23 | 24 | div#text-based-window-manager { 25 | clear: both; 26 | } 27 | 28 | @media screen and (max-width: 768px) { 29 | #fork-gh img { 30 | display: none; 31 | } 32 | } 33 | 34 | table.docutils { 35 | background-color: #fafbfc; 36 | border: 0; 37 | } 38 | 39 | table.docutils td, table.docutils th { 40 | border: 0; 41 | } 42 | 43 | table.docutils pre { 44 | background-color: rgba(239, 242, 244, .75); 45 | } 46 | 47 | pre { 48 | background-color: #fafbfc; 49 | border-left: 5px solid #558abb; 50 | font-size: 0.75em; 51 | } 52 | 53 | div.seealso, div.note { 54 | background-color: #fafbfc; 55 | border-right: 0; 56 | border-top: 0; 57 | border-bottom: 0; 58 | } 59 | 60 | div.seealso { 61 | border-left: 5px solid #8abb55; 62 | } 63 | 64 | div.note { 65 | border-left: 5px solid #bb5557; 66 | } 67 | 68 | code.literal { 69 | font-size: 85%; 70 | color: #24292e; 71 | box-sizing: border-box; 72 | display: inline-block; 73 | padding: 0; 74 | background: #fafcfc; 75 | border: 1px solid #f0f4f7; 76 | line-height: 20px; 77 | } 78 | 79 | code::before, code::after { 80 | letter-spacing: -0.2em; 81 | content: "\00a0"; 82 | } 83 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-256x256.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-36x36.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-384x384.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-48x48.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-72x72.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-chrome-96x96.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-icon.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/favicon-96x96.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/favicon.ico -------------------------------------------------------------------------------- /docs/_static/img/icons/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 22 | -------------------------------------------------------------------------------- /docs/_static/img/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/logo.png -------------------------------------------------------------------------------- /docs/_static/img/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | -------------------------------------------------------------------------------- /docs/_static/img/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /docs/_static/img/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /docs/_static/img/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /docs/_static/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/docs/_static/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /docs/_static/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 14 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {# Import the theme's layout. #} 2 | {% extends "!layout.html" %} 3 | 4 | {# Include our new CSS file into existing ones. #} 5 | {% set css_files = css_files + ['_static/django-docutils.css']%} 6 | 7 | 8 | {%- block extrahead %} 9 | {{ super() }} 10 | {%- if theme_show_meta_manifest_tag == true %} 11 | 12 | {% endif -%} 13 | {%- if theme_show_meta_og_tags == true %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% endif -%} 30 | {%- if theme_show_meta_app_icon_tags == true %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% endif -%} 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /docs/_templates/more.html: -------------------------------------------------------------------------------- 1 |

Other Projects

2 | 3 |

4 | 5 |

More open source projects from Tony Narlock:

6 | 7 | 13 | 14 | 15 | Fork me on GitHub 18 | 19 | -------------------------------------------------------------------------------- /docs/_templates/sidebar/projects.html: -------------------------------------------------------------------------------- 1 | 56 | 70 | -------------------------------------------------------------------------------- /docs/_templates/star.html: -------------------------------------------------------------------------------- 1 |

2 | 4 |

5 | -------------------------------------------------------------------------------- /docs/api/exc.md: -------------------------------------------------------------------------------- 1 | (api_exc)= 2 | 3 | # `exc` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.exc 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | (api)= 2 | 3 | # API Reference 4 | 5 | :::{seealso} 6 | 7 | {ref}`Quickstart `. 8 | 9 | ::: 10 | 11 | ## Inside 12 | 13 | ```{toctree} 14 | :maxdepth: 1 15 | 16 | exc 17 | views 18 | lib/index 19 | template 20 | templatetags/index 21 | ``` 22 | 23 | ## Common 24 | 25 | ### Exceptions 26 | 27 | ```{eval-rst} 28 | .. autoexception:: django_docutils.exc.DjangoDocutilsException 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/api/lib/directives/code.md: -------------------------------------------------------------------------------- 1 | (api_lib_directives_code)= 2 | 3 | # `lib.directives.code` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.directives.code 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/directives/index.md: -------------------------------------------------------------------------------- 1 | (api_lib_directives)= 2 | 3 | # `lib.directives` 4 | 5 | ```{toctree} 6 | :maxdepth: 1 7 | 8 | code 9 | registry 10 | ``` 11 | 12 | ```{eval-rst} 13 | .. automodule:: django_docutils.lib.directives 14 | :members: 15 | :private-members: 16 | :show-inheritance: 17 | :member-order: bysource 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/api/lib/directives/registry.md: -------------------------------------------------------------------------------- 1 | (api_lib_directives_registry)= 2 | 3 | # `lib.directives.registry` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.directives.registry 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/index.md: -------------------------------------------------------------------------------- 1 | (api_lib)= 2 | 3 | # `lib` 4 | 5 | ```{toctree} 6 | :maxdepth: 1 7 | 8 | directives/index 9 | metadata/index 10 | publisher 11 | roles/index 12 | settings 13 | text 14 | transforms/index 15 | utils 16 | views 17 | writers 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/api/lib/metadata/extract.md: -------------------------------------------------------------------------------- 1 | (api_lib_metadata_extract)= 2 | 3 | # `lib.metadata.extract` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.metadata.extract 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/metadata/index.md: -------------------------------------------------------------------------------- 1 | (api_lib_metadata)= 2 | 3 | # `lib.metadata` 4 | 5 | ```{toctree} 6 | :maxdepth: 1 7 | 8 | extract 9 | process 10 | processors 11 | ``` 12 | 13 | ```{eval-rst} 14 | .. automodule:: django_docutils.lib.metadata 15 | :members: 16 | :private-members: 17 | :show-inheritance: 18 | :member-order: bysource 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/api/lib/metadata/process.md: -------------------------------------------------------------------------------- 1 | (api_lib_metadata_process)= 2 | 3 | # `lib.metadata.process` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.metadata.process 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/metadata/processors.md: -------------------------------------------------------------------------------- 1 | (api_lib_metadata_processors)= 2 | 3 | # `lib.metadata.processors` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.metadata.processors 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/publisher.md: -------------------------------------------------------------------------------- 1 | (api_lib_publisher)= 2 | 3 | # `lib.publisher` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.publisher 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/common.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_common)= 2 | 3 | # `lib.roles.common` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.common 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/email.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_email)= 2 | 3 | # `lib.roles.email` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.email 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/file.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_file)= 2 | 3 | # `lib.roles.file` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.file 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/github.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_github)= 2 | 3 | # `lib.roles.github` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.github 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/hackernews.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_hackernews)= 2 | 3 | # `lib.roles.hackernews` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.hackernews 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/index.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles)= 2 | 3 | # `lib.roles` 4 | 5 | ## Core 6 | 7 | ```{toctree} 8 | :maxdepth: 1 9 | 10 | common 11 | registry 12 | types 13 | ``` 14 | 15 | ## Custom roles 16 | 17 | ```{toctree} 18 | :maxdepth: 1 19 | 20 | email 21 | file 22 | github 23 | hackernews 24 | kbd 25 | leanpub 26 | pypi 27 | readthedocs 28 | twitter 29 | url 30 | wikipedia 31 | ``` 32 | 33 | ```{eval-rst} 34 | .. automodule:: django_docutils.lib.roles 35 | :members: 36 | :private-members: 37 | :show-inheritance: 38 | :member-order: bysource 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/api/lib/roles/kbd.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_kbd)= 2 | 3 | # `lib.roles.kbd` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.kbd 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/leanpub.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_leanpub)= 2 | 3 | # `lib.roles.leanpub` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.leanpub 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/pypi.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_pypi)= 2 | 3 | # `lib.roles.pypi` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.pypi 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/readthedocs.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_readthedocs)= 2 | 3 | # `lib.roles.readthedocs` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.readthedocs 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/registry.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_registry)= 2 | 3 | # `lib.roles.registry` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.registry 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/twitter.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_twitter)= 2 | 3 | # `lib.roles.twitter` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.twitter 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/types.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_types)= 2 | 3 | # `lib.roles.types` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.types 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/url.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_url)= 2 | 3 | # `lib.roles.url` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.url 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/roles/wikipedia.md: -------------------------------------------------------------------------------- 1 | (api_lib_roles_wikipedia)= 2 | 3 | # `lib.roles.wikipedia` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.roles.wikipedia 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/settings.md: -------------------------------------------------------------------------------- 1 | (api_lib_settings)= 2 | 3 | # `lib.settings` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.settings 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :undoc-members: 11 | :member-order: bysource 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/api/lib/text.md: -------------------------------------------------------------------------------- 1 | (api_lib_text)= 2 | 3 | # `lib.text` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.text 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/transforms/code.md: -------------------------------------------------------------------------------- 1 | (api_lib_transforms_code)= 2 | 3 | # `lib.roles.transforms.code` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.transforms.code 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/api/lib/transforms/index.md: -------------------------------------------------------------------------------- 1 | (api_lib_transforms)= 2 | 3 | # `lib.transforms` 4 | 5 | ```{toctree} 6 | :maxdepth: 1 7 | 8 | code 9 | toc 10 | ``` 11 | 12 | ```{eval-rst} 13 | .. automodule:: django_docutils.lib.transforms 14 | :members: 15 | :private-members: 16 | :show-inheritance: 17 | :member-order: bysource 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/api/lib/transforms/toc.md: -------------------------------------------------------------------------------- 1 | (api_lib_transforms_toc)= 2 | 3 | # `lib.roles.transforms.toc` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.transforms.toc 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/api/lib/utils.md: -------------------------------------------------------------------------------- 1 | (api_lib_utils)= 2 | 3 | # `lib.utils` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.utils 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/views.md: -------------------------------------------------------------------------------- 1 | (api_lib_views)= 2 | 3 | # `lib.views` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.views 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :member-order: bysource 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/api/lib/writers.md: -------------------------------------------------------------------------------- 1 | (api_lib_writers)= 2 | 3 | # `lib.writers` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.lib.writers 7 | :members: 8 | :private-members: 9 | :show-inheritance: 10 | :undoc-members: 11 | :member-order: bysource 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/api/template.md: -------------------------------------------------------------------------------- 1 | (api_template)= 2 | 3 | # `template` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.template 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/api/templatetags/django_docutils.md: -------------------------------------------------------------------------------- 1 | (api_templatetags_django_docutils)= 2 | 3 | # `templatetags.django_docutils` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.templatetags.django_docutils 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/api/templatetags/index.md: -------------------------------------------------------------------------------- 1 | (api_templatetags)= 2 | 3 | # `templatetags` 4 | 5 | ```{toctree} 6 | :maxdepth: 1 7 | 8 | django_docutils 9 | ``` 10 | 11 | ```{eval-rst} 12 | .. automodule:: django_docutils.templatetags 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/api/views.md: -------------------------------------------------------------------------------- 1 | (api_views)= 2 | 3 | # `views` 4 | 5 | ```{eval-rst} 6 | .. automodule:: django_docutils.views 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/class_based_view.md: -------------------------------------------------------------------------------- 1 | (class_based_view)= 2 | 3 | # Class-based view 4 | 5 | ## Setup 6 | 7 | :::{seealso} 8 | 9 | {ref}`Quickstart ` 10 | ::: 11 | 12 | You can also use a class-based view to render reStructuredText (reST). 13 | 14 | If you want to use reStructuredText as a django template engine, `INSTALLED_APPS` _isn't_ required, 15 | instead you add this to your `TEMPLATES` variable in your settings: 16 | 17 | ```python 18 | TEMPLATES = [ 19 | # ... Other engines 20 | { 21 | "NAME": "docutils", 22 | "BACKEND": "django_docutils.template.DocutilsTemplates", 23 | "DIRS": [], 24 | "APP_DIRS": True, 25 | } 26 | ] 27 | ``` 28 | 29 | ## Introduction to views 30 | 31 | Now django will be able to scan for .rst files and process them. In your view: 32 | 33 | ```python 34 | from django_docutils.views import DocutilsView 35 | 36 | class HomeView(DocutilsView): 37 | template_name = 'base.html' 38 | rst_name = 'home.rst' 39 | ``` 40 | 41 | *yourapp/templates/home.rst*: 42 | 43 | ````restructuredtext 44 | hey 45 | --- 46 | 47 | hi 48 | ## 49 | 50 | A. hows 51 | B. it 52 | 53 | C. going 54 | D. today 55 | 56 | **hi** 57 | *hi* 58 | ```` 59 | 60 | *yourapp/templates/base.html*: 61 | 62 | ```django 63 | {{content}} 64 | ``` 65 | 66 | Output: 67 | 68 | ```html 69 |
70 |

hey

71 |

hi

72 |
    73 |
  1. hows
  2. 74 |
  3. it
  4. 75 |
  5. going
  6. 76 |
  7. today
  8. 77 |
78 |

hi 79 | hi

80 |
81 | ``` 82 | 83 | :::{admonition} Explore the API 84 | 85 | - {class}`~django_docutils.views.DocutilsView`, {class}`~django_docutils.views.DocutilsResponse` 86 | - {class}`~django_docutils.lib.views.RSTMixin`, 87 | {class}`~django_docutils.lib.views.RSTRawView`, {class}`~django_docutils.lib.views.RSTView` 88 | - {class}`~django_docutils.template.DocutilsTemplates`, {class}`~django_docutils.template.DocutilsTemplate` 89 | 90 | ::: 91 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Sphinx configuration for Django Docutils.""" 2 | 3 | # flake8: noqa: E501 4 | from __future__ import annotations 5 | 6 | import inspect 7 | import os 8 | import pathlib 9 | import sys 10 | import typing as t 11 | from os.path import relpath 12 | 13 | import django 14 | 15 | import django_docutils 16 | 17 | if t.TYPE_CHECKING: 18 | from sphinx.application import Sphinx 19 | 20 | # Get the project root dir, which is the parent dir of this 21 | cwd = pathlib.Path(__file__).parent 22 | project_root = cwd.parent 23 | src_root = project_root / "src" 24 | 25 | sys.path.insert(0, str(project_root)) 26 | sys.path.insert(0, str(src_root)) 27 | sys.path.insert(0, str(cwd / "_ext")) 28 | 29 | os.environ["DJANGO_SETTINGS_MODULE"] = "django_docutils.lib.settings" 30 | django.setup() 31 | 32 | # package data 33 | about: dict[str, str] = {} 34 | with (src_root / "django_docutils" / "__about__.py").open() as fp: 35 | exec(fp.read(), about) 36 | 37 | extensions = [ 38 | "sphinx.ext.autodoc", 39 | "sphinx.ext.intersphinx", 40 | "sphinx.ext.linkcode", 41 | "sphinx.ext.napoleon", 42 | "sphinx_autodoc_typehints", 43 | "sphinx_click.ext", # sphinx-click 44 | "sphinx_inline_tabs", 45 | "sphinx_copybutton", 46 | "sphinxext.opengraph", 47 | "sphinxext.rediraffe", 48 | "myst_parser", 49 | "linkify_issues", 50 | ] 51 | myst_enable_extensions = [ 52 | "colon_fence", 53 | "substitution", 54 | "replacements", 55 | "strikethrough", 56 | "linkify", 57 | ] 58 | 59 | templates_path = ["_templates"] 60 | 61 | source_suffix = {".rst": "restructuredtext", ".md": "markdown"} 62 | 63 | master_doc = "index" 64 | 65 | project = about["__title__"] 66 | project_copyright = about["__copyright__"] 67 | 68 | version = "{}".format(".".join(about["__version__"].split("."))[:2]) 69 | release = "{}".format(about["__version__"]) 70 | 71 | exclude_patterns = ["_build"] 72 | 73 | pygments_style = "monokai" 74 | pygments_dark_style = "monokai" 75 | 76 | html_favicon = "_static/favicon.ico" 77 | html_static_path = ["_static"] 78 | html_css_files = ["css/custom.css"] 79 | html_extra_path = ["manifest.json"] 80 | html_theme = "furo" 81 | html_theme_path: list[str] = [] 82 | html_theme_options: dict[str, str | list[dict[str, str]]] = { 83 | "light_logo": "img/icons/logo.svg", 84 | "dark_logo": "img/icons/logo-dark.svg", 85 | "footer_icons": [ 86 | { 87 | "name": "GitHub", 88 | "url": about["__github__"], 89 | "html": """ 90 | 91 | 92 | 93 | """, 94 | "class": "", 95 | }, 96 | ], 97 | "source_repository": f"{about['__github__']}/", 98 | "source_branch": "master", 99 | "source_directory": "docs/", 100 | } 101 | 102 | html_sidebars = { 103 | "**": [ 104 | "sidebar/scroll-start.html", 105 | "sidebar/brand.html", 106 | "sidebar/search.html", 107 | "sidebar/navigation.html", 108 | "sidebar/projects.html", 109 | "sidebar/scroll-end.html", 110 | ], 111 | } 112 | 113 | # linkify_issues 114 | issue_url_tpl = about["__github__"] + "/issues/{issue_id}" 115 | 116 | # sphinx.ext.autodoc 117 | autodoc_mock_imports = ["django"] 118 | toc_object_entries_show_parents = "hide" 119 | autodoc_default_options = { 120 | "undoc-members": True, 121 | "members": True, 122 | "private-members": True, 123 | "show-inheritance": True, 124 | "member-order": "bysource", 125 | } 126 | 127 | # sphinxext.opengraph 128 | ogp_site_url = about["__docs__"] 129 | ogp_image = "_static/img/icons/icon-192x192.png" 130 | ogp_site_name = about["__title__"] 131 | 132 | # sphinx-copybutton 133 | copybutton_prompt_text = ( 134 | r">>> |\.\.\. |> |\$ |\# | In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " 135 | ) 136 | copybutton_prompt_is_regexp = True 137 | copybutton_remove_prompts = True 138 | 139 | # sphinxext-rediraffe 140 | rediraffe_redirects = "redirects.txt" 141 | rediraffe_branch = "master~1" 142 | 143 | intersphinx_mapping = { 144 | "python": ("http://docs.python.org/", None), 145 | "django": ( 146 | "https://docs.djangoproject.com/en/4.2/", 147 | "https://docs.djangoproject.com/en/4.2/_objects/", 148 | ), 149 | } 150 | 151 | 152 | def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str: 153 | """ 154 | Determine the URL corresponding to Python object. 155 | 156 | Notes 157 | ----- 158 | From https://github.com/numpy/numpy/blob/v1.15.1/doc/source/conf.py, 7c49cfa 159 | on Jul 31. License BSD-3. https://github.com/numpy/numpy/blob/v1.15.1/LICENSE.txt 160 | """ 161 | if domain != "py": 162 | return None 163 | 164 | modname = info["module"] 165 | fullname = info["fullname"] 166 | 167 | submod = sys.modules.get(modname) 168 | if submod is None: 169 | return None 170 | 171 | obj = submod 172 | for part in fullname.split("."): 173 | try: 174 | obj = getattr(obj, part) 175 | except Exception: # noqa: PERF203 176 | return None 177 | 178 | # strip decorators, which would resolve to the source of the decorator 179 | # possibly an upstream bug in getsourcefile, bpo-1764286 180 | try: 181 | unwrap = inspect.unwrap 182 | except AttributeError: 183 | pass 184 | else: 185 | if callable(obj): 186 | obj = unwrap(obj) 187 | 188 | try: 189 | fn = inspect.getsourcefile(obj) 190 | except Exception: 191 | fn = None 192 | if not fn: 193 | return None 194 | 195 | try: 196 | source, lineno = inspect.getsourcelines(obj) 197 | except Exception: 198 | lineno = None 199 | 200 | linespec = f"#L{lineno}-L{lineno + len(source) - 1}" if lineno else "" 201 | 202 | fn = relpath(fn, start=pathlib.Path(django_docutils.__file__).parent) 203 | 204 | if "dev" in about["__version__"]: 205 | return "{}/blob/master/{}/{}/{}{}".format( 206 | about["__github__"], 207 | "src", 208 | about["__package_name__"], 209 | fn, 210 | linespec, 211 | ) 212 | return "{}/blob/v{}/{}/{}/{}{}".format( 213 | about["__github__"], 214 | about["__version__"], 215 | "src", 216 | about["__package_name__"], 217 | fn, 218 | linespec, 219 | ) 220 | 221 | 222 | def remove_tabs_js(app: Sphinx, exc: Exception) -> None: 223 | """Fix for sphinx-inline-tabs#18.""" 224 | if app.builder.format == "html" and not exc: 225 | tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js" 226 | tabs_js.unlink(missing_ok=True) 227 | 228 | 229 | def setup(app: Sphinx) -> None: 230 | """Sphinx setup hook.""" 231 | app.connect("build-finished", remove_tabs_js) 232 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | (faq)= 2 | 3 | # FAQ 4 | 5 | ## General 6 | 7 | ### What is reST, RST, reStructuredText? 8 | 9 | [reStructuredText] is a markup syntax, similar to markdown. 10 | 11 | ### What is docutils? 12 | 13 | [docutils] is a python package for parsing and publishing markup. The default docutils package 14 | supports reStructuredText. It can also be extended to parse markdown 15 | (e.g. [myst-parser]). 16 | 17 | [myst-parser]: https://github.com/executablebooks/MyST-Parser 18 | 19 | ## Django Docutils 20 | 21 | ### Do I need this package to parse reStructuredText in Django? 22 | 23 | No! [docutils] can always be used directly. 24 | 25 | This package simply offers template extensions to use docutils in django views. 26 | 27 | ### What does this package provide? 28 | 29 | 3 ways to render reStructuredText via docutils in [Django]: 30 | 31 | 1. {ref}`template_tag` 32 | 2. {ref}`template_filter` 33 | 3. {ref}`class_based_view` 34 | 35 | ### Can I copy code from this project to my own? 36 | 37 | Yes! Go ahead, the project's source is released under the [MIT license] - you are welcome to view the codebase and copy just 38 | what you need. 39 | 40 | [MIT license]: https://github.com/tony/django-docutils/blob/master/LICENSE 41 | [docutils]: https://docutils.sourceforge.io/ 42 | [reStructuredText]: https://docutils.sourceforge.io/rst.html 43 | [Django]: https://docs.djangoproject.com/ 44 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | (history)= 2 | 3 | ```{include} ../CHANGES 4 | 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | (index)= 2 | 3 | ```{include} ../README.md 4 | 5 | ``` 6 | 7 | ```{toctree} 8 | :maxdepth: 2 9 | :hidden: 10 | 11 | quickstart 12 | template_tag 13 | template_filter 14 | class_based_view 15 | faq 16 | ``` 17 | 18 | ```{toctree} 19 | :caption: Project 20 | :hidden: 21 | 22 | api/index 23 | history 24 | GitHub 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-docutils", 3 | "short_name": "django-docutils", 4 | "description": "Helpers for interfacing between django and docutils", 5 | "theme_color": "#2196f3", 6 | "background_color": "#fff", 7 | "display": "browser", 8 | "Scope": "https://django-docutils.git-pull.com/", 9 | "start_url": "https://django-docutils.git-pull.com/", 10 | "icons": [ 11 | { 12 | "src": "_static/img/icons/android-chrome-72x72.png", 13 | "sizes": "72x72", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "_static/img/icons/android-chrome-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "_static/img/icons/android-chrome-128x128.png", 23 | "sizes": "128x128", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "_static/img/icons/android-chrome-144x144.png", 28 | "sizes": "144x144", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "_static/img/icons/android-chrome-152x152.png", 33 | "sizes": "152x152", 34 | "type": "image/png" 35 | }, 36 | { 37 | "src": "_static/img/icons/android-chrome-192x192.png", 38 | "sizes": "192x192", 39 | "type": "image/png" 40 | }, 41 | { 42 | "src": "_static/img/icons/android-chrome-384x384.png", 43 | "sizes": "384x384", 44 | "type": "image/png" 45 | }, 46 | { 47 | "src": "_static/img/icons/android-chrome-512x512.png", 48 | "sizes": "512x512", 49 | "type": "image/png" 50 | } 51 | ], 52 | "splash_pages": null 53 | } 54 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | (quickstart)= 2 | 3 | # Quickstart 4 | 5 | ## Installation 6 | 7 | For latest official version: 8 | 9 | ```console 10 | $ pip install django-docutils 11 | ``` 12 | 13 | Upgrading: 14 | 15 | ```console 16 | $ pip install --upgrade django-docutils 17 | ``` 18 | 19 | (developmental-releases)= 20 | 21 | ### Developmental releases 22 | 23 | New versions of django-docutils are published to PyPI as alpha, beta, or release candidates. In 24 | their versions you will see notification like `a1`, `b1`, and `rc1`, respectively. `1.10.0b4` would 25 | mean the 4th beta release of `1.10.0` before general availability. 26 | 27 | - [pip]\: 28 | 29 | ```console 30 | $ pip install --upgrade --pre django-docutils 31 | ``` 32 | 33 | via trunk (can break easily): 34 | 35 | - [pip]\: 36 | 37 | ```console 38 | $ pip install -e git+https://github.com/tony/django-docutils.git#egg=django-docutils 39 | ``` 40 | 41 | [pip]: https://pip.pypa.io/en/stable/ 42 | 43 | ## Add the django app 44 | 45 | Next, add `django_docutils` to your `INSTALLED_APPS` in your settings file: 46 | 47 | ```python 48 | INSTALLED_APPS = [ 49 | # ... your default apps, 50 | 'django_docutils' 51 | ] 52 | ``` 53 | 54 | ## Next steps 55 | 56 | Integate docutils to your django site: 57 | 58 | 1. {ref}`template_tag` 59 | 2. {ref}`template_filter` 60 | 3. {ref}`class_based_view` 61 | 62 | ::: 63 | -------------------------------------------------------------------------------- /docs/redirects.txt: -------------------------------------------------------------------------------- 1 | "usage.rst" "quickstart.md" 2 | "api.md" "api/index.md" 3 | "django_view.rst" "class_based_view.md" 4 | -------------------------------------------------------------------------------- /docs/template_filter.md: -------------------------------------------------------------------------------- 1 | (template_filter)= 2 | 3 | # Template filter 4 | 5 | ## Setup 6 | 7 | :::{seealso} 8 | 9 | {ref}`Quickstart ` 10 | 11 | ::: 12 | 13 | Make sure `django_docutils` is added your `INSTALLED_APPS` in your settings file: 14 | 15 | ```python 16 | INSTALLED_APPS = [ 17 | # ... your default apps, 18 | 'django_docutils' 19 | ] 20 | ``` 21 | 22 | ## Using the django filter 23 | 24 | In your HTML template: 25 | 26 | ```django 27 | {% load django_docutils %} 28 | {% filter rst %} 29 | # hey 30 | # how's it going 31 | A. hows 32 | B. it 33 | 34 | C. going 35 | D. today 36 | 37 | **hi** 38 | *hi* 39 | {% endfilter %} 40 | ``` 41 | 42 | Output: 43 | 44 | ```html 45 |
46 |

hey

47 |

hi

48 |
    49 |
  1. hows

  2. 50 |
  3. it

  4. 51 |
  5. going

  6. 52 |
  7. today

  8. 53 |
54 |

hi 55 | hi

56 |
57 | ``` 58 | 59 | :::{admonition} Explore the API 60 | 61 | - {func}`~django_docutils.templatetags.django_docutils.rst_filter` 62 | 63 | ::: 64 | -------------------------------------------------------------------------------- /docs/template_tag.md: -------------------------------------------------------------------------------- 1 | (template_tag)= 2 | 3 | # Template tag 4 | 5 | ## Setup 6 | 7 | :::{seealso} 8 | 9 | {ref}`Quickstart ` 10 | 11 | ::: 12 | 13 | Make sure `django_docutils` is added your `INSTALLED_APPS` in your settings file: 14 | 15 | ```python 16 | INSTALLED_APPS = [ 17 | # ... your default apps, 18 | 'django_docutils' 19 | ] 20 | ``` 21 | 22 | ## Using the django tag 23 | 24 | In your HTML template: 25 | 26 | ```django 27 | {% load django_docutils %} 28 | {% rst %} 29 | # hey 30 | # how's it going 31 | A. hows 32 | B. it 33 | 34 | C. going 35 | D. today 36 | 37 | **hi** 38 | *hi* 39 | {% endrst %} 40 | ``` 41 | 42 | Output: 43 | 44 | ```html 45 |
46 |

hey

47 |

hi

48 |
    49 |
  1. hows

  2. 50 |
  3. it

  4. 51 |
  5. going

  6. 52 |
  7. today

  8. 53 |
54 |

hi 55 | hi

56 |
57 | ``` 58 | 59 | :::{admonition} Explore the API 60 | 61 | - {func}`~django_docutils.templatetags.django_docutils.rst` 62 | 63 | ::: 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-docutils" 3 | version = "0.28.0" 4 | description = "Docutils (a.k.a. reStructuredText, reST, RST) support for django." 5 | requires-python = ">=3.9,<4.0" 6 | authors = [ 7 | {name = "Tony Narlock", email = "tony@git-pull.com"} 8 | ] 9 | license = { text = "MIT" } 10 | readme = "README.md" 11 | classifiers = [ 12 | 'Development Status :: 2 - Pre-Alpha', 13 | 'Framework :: Django', 14 | 'Framework :: Django :: 4.2', 15 | 'Framework :: Django :: 5.0', 16 | 'Framework :: Django :: 5.1', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Natural Language :: English', 20 | 'Programming Language :: Python :: 3', 21 | 'Programming Language :: Python :: 3.9', 22 | 'Programming Language :: Python :: 3.10', 23 | 'Programming Language :: Python :: 3.11', 24 | 'Programming Language :: Python :: 3.12', 25 | 'Programming Language :: Python :: 3.13', 26 | "Typing :: Typed", 27 | ] 28 | keywords = ["django", "docutils", "reStructuredText", "rst", "reST"] 29 | homepage = "https://django-docutils.git-pull.com" 30 | packages = [ 31 | { include = "*", from = "src" }, 32 | ] 33 | include = [ 34 | { path = "tests", format = "sdist" }, 35 | ] 36 | dependencies = [ 37 | "django>=4.2", 38 | "docutils", 39 | "pygments<3" 40 | ] 41 | 42 | [tool.project] 43 | "Bug Tracker" = "https://github.com/tony/django-docutils/issues" 44 | Documentation = "https://django-docutils.git-pull.com" 45 | Repository = "https://github.com/tony/django-docutils" 46 | Changes = "https://github.com/tony/django-docutils/blob/master/CHANGES" 47 | "Q & A" = "https://github.com/tony/django-docutils/discussions" 48 | 49 | [project.optional-dependencies] 50 | pytz = [ 51 | "pytz" 52 | ] 53 | 54 | [tool.uv] 55 | dev-dependencies = [ 56 | # Docs 57 | "sphinx", 58 | "furo", 59 | "gp-libs", 60 | "sphinx-autobuild", 61 | "sphinx-autodoc-typehints", 62 | "sphinx-inline-tabs", 63 | "sphinxext-opengraph", 64 | "sphinx-copybutton", 65 | "sphinxext-rediraffe", 66 | "sphinx-click", 67 | "myst-parser", 68 | "linkify-it-py", 69 | # Testing 70 | "gp-libs", 71 | "pytest", 72 | "pytest-rerunfailures", 73 | "pytest-mock", 74 | "pytest-watcher", 75 | "pytest-django", 76 | "dj-inmemorystorage", 77 | # Coverage 78 | "codecov", 79 | "coverage", 80 | "pytest-cov", 81 | # Lint 82 | "ruff", 83 | "mypy", 84 | "django-stubs", 85 | "docutils-stubs", 86 | "types-docutils", 87 | "types-Pygments", 88 | "types-pytz", 89 | ] 90 | 91 | [dependency-groups] 92 | docs = [ 93 | "sphinx", 94 | "furo", 95 | "gp-libs", 96 | "sphinx-autobuild", 97 | "sphinx-autodoc-typehints", 98 | "sphinx-inline-tabs", 99 | "sphinxext-opengraph", 100 | "sphinx-copybutton", 101 | "sphinxext-rediraffe", 102 | "sphinx-click", 103 | "myst-parser", 104 | "linkify-it-py", 105 | ] 106 | testing = [ 107 | "gp-libs", 108 | "pytest", 109 | "pytest-rerunfailures", 110 | "pytest-mock", 111 | "pytest-watcher", 112 | "pytest-django", 113 | "dj-inmemorystorage" 114 | ] 115 | coverage =[ 116 | "codecov", 117 | "coverage", 118 | "pytest-cov", 119 | ] 120 | lint = [ 121 | "ruff", 122 | "mypy", 123 | "django-stubs", 124 | "docutils-stubs", 125 | "types-docutils", 126 | "types-Pygments", 127 | "types-pytz", 128 | ] 129 | 130 | [build-system] 131 | requires = ["hatchling"] 132 | build-backend = "hatchling.build" 133 | 134 | [tool.mypy] 135 | strict = true 136 | plugins = ["mypy_django_plugin.main"] 137 | python_version = "3.9" 138 | files = [ 139 | "src/", 140 | "tests/", 141 | ] 142 | enable_incomplete_feature = [] 143 | 144 | [tool.django-stubs] 145 | django_settings_module = "django_docutils.lib.settings" 146 | 147 | [[tool.mypy.overrides]] 148 | module = [ 149 | "inmemorystorage.*", 150 | "django_docutils.*", 151 | ] 152 | ignore_missing_imports = true 153 | 154 | [[tool.mypy.overrides]] 155 | module = ["django_docutils.lib.publisher"] 156 | disable_error_code = [ 157 | "unused-ignore", # Unused "type: ignore" comment 158 | "arg-type", # Argument type incompatibility 159 | "union-attr", # Attribute access on union types 160 | "redundant-cast", # Redundant cast 161 | "var-annotated", # Need type annotation for "reader" 162 | "no-any-return" # Returning Any from function declared to return "document" 163 | ] 164 | 165 | [[tool.mypy.overrides]] 166 | module = ["django_docutils.lib.roles.common"] 167 | disable_error_code = [ 168 | "call-arg" # Too many arguments for "Text" 169 | ] 170 | 171 | [[tool.mypy.overrides]] 172 | module = ["django_docutils.lib.metadata.extract"] 173 | disable_error_code = [ 174 | "misc" # Need more than 0 values to unpack (2 expected) 175 | ] 176 | 177 | [tool.ruff] 178 | target-version = "py39" 179 | 180 | [tool.ruff.lint] 181 | select = [ 182 | "E", # pycodestyle 183 | "F", # pyflakes 184 | "I", # isort 185 | "UP", # pyupgrade 186 | "A", # flake8-builtins 187 | "B", # flake8-bugbear 188 | "C4", # flake8-comprehensions 189 | "COM", # flake8-commas 190 | "EM", # flake8-errmsg 191 | "Q", # flake8-quotes 192 | "PTH", # flake8-use-pathlib 193 | "SIM", # flake8-simplify 194 | "TRY", # Trycertatops 195 | "PERF", # Perflint 196 | "RUF", # Ruff-specific rules 197 | "D", # pydocstyle 198 | "FA100", # future annotations 199 | ] 200 | ignore = [ 201 | "COM812", # missing trailing comma, ruff format conflict 202 | ] 203 | extend-safe-fixes = [ 204 | "UP006", 205 | "UP007", 206 | ] 207 | pyupgrade.keep-runtime-typing = false 208 | 209 | [tool.ruff.lint.isort] 210 | known-first-party = [ 211 | "django_docutils", 212 | ] 213 | combine-as-imports = true 214 | required-imports = [ 215 | "from __future__ import annotations", 216 | ] 217 | 218 | [tool.ruff.lint.flake8-builtins] 219 | builtins-allowed-modules = [ 220 | "code", 221 | "dataclasses", 222 | "email", 223 | "types", 224 | ] 225 | 226 | [tool.ruff.lint.pydocstyle] 227 | convention = "numpy" 228 | 229 | [tool.ruff.lint.per-file-ignores] 230 | "*/__init__.py" = ["F401"] 231 | "src/**/*.py" = ["PTH"] 232 | 233 | [tool.pytest.ini_options] 234 | addopts = "--tb=short --no-header --showlocals --doctest-modules" 235 | doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE" 236 | testpaths = [ 237 | "src/django_docutils", 238 | "tests", 239 | "docs", 240 | ] 241 | pythonpath = ". tests" 242 | filterwarnings = [ 243 | "ignore::PendingDeprecationWarning", 244 | "ignore:The frontend.Option(Parser)? class.*:DeprecationWarning::", 245 | ] 246 | 247 | # pytest-django 248 | django_find_project = false 249 | DJANGO_SETTINGS_MODULE = 'tests.settings' 250 | 251 | [tool.coverage.run] 252 | omit = [ 253 | "docs/conf.py", 254 | ] 255 | 256 | [tool.coverage.report] 257 | exclude_also = [ 258 | "def __repr__", 259 | "if self.debug:", 260 | "if settings.DEBUG", 261 | "raise AssertionError", 262 | "raise NotImplementedError", 263 | "if __name__ == .__main__.:", 264 | "if TYPE_CHECKING:", 265 | "if t.TYPE_CHECKING:", 266 | "class .*\\bProtocol\\):", 267 | "@(abc\\.)?abstractmethod", 268 | "from __future__ import annotations", 269 | ] 270 | -------------------------------------------------------------------------------- /src/django_docutils/__about__.py: -------------------------------------------------------------------------------- 1 | """Metadata package for Django Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | __title__ = "django-docutils" 6 | __package_name__ = "django_docutils" 7 | __description__ = "Docutils (a.k.a. reStructuredText, reST, RST) support for django." 8 | __version__ = "0.28.0" 9 | __author__ = "Tony Narlock" 10 | __github__ = "https://github.com/tony/django-docutils" 11 | __pypi__ = "https://pypi.org/project/django-docutils/" 12 | __docs__ = "https://django-docutils.git-pull.com" 13 | __tracker__ = "https://github.com/tony/django-docutils/issues" 14 | __email__ = "tony@git-pull.com" 15 | __license__ = "MIT" 16 | __copyright__ = "Copyright 2013- Tony Narlock" 17 | -------------------------------------------------------------------------------- /src/django_docutils/__init__.py: -------------------------------------------------------------------------------- 1 | """Django Docutils package for rendering Docutils in Django.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/_internal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/src/django_docutils/_internal/__init__.py -------------------------------------------------------------------------------- /src/django_docutils/_internal/types.py: -------------------------------------------------------------------------------- 1 | """Internal :term:`type annotations `. 2 | 3 | Notes 4 | ----- 5 | :class:`StrPath` and :class:`StrOrBytesPath` is based on `typeshed's`_. 6 | 7 | .. _typeshed's: https://github.com/python/typeshed/blob/5df8de7/stdlib/_typeshed/__init__.pyi#L115-L118 8 | """ # E501 9 | 10 | from __future__ import annotations 11 | 12 | import typing as t 13 | 14 | if t.TYPE_CHECKING: 15 | from os import PathLike 16 | 17 | from typing_extensions import TypeAlias 18 | 19 | StrPath: TypeAlias = t.Union[str, "PathLike[str]"] # stable 20 | """:class:`os.PathLike` or :class:`str`""" 21 | 22 | StrOrBytesPath: TypeAlias = t.Union[ 23 | str, 24 | bytes, 25 | "PathLike[str]", 26 | "PathLike[bytes]", # stable 27 | ] 28 | """:class:`os.PathLike`, :class:`str` or :term:`bytes-like object`""" 29 | -------------------------------------------------------------------------------- /src/django_docutils/exc.py: -------------------------------------------------------------------------------- 1 | """Exceptions for Django Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | 6 | class DjangoDocutilsException(Exception): 7 | """Base exception for Django Docutils package.""" 8 | 9 | 10 | class DocutilsNotInstalled(DjangoDocutilsException): 11 | """Docutils is not installed.""" 12 | 13 | def __init__( 14 | self, 15 | message: str = "The Python docutils library isn't installed", 16 | *args: object, 17 | **kwargs: object, 18 | ) -> None: 19 | return super().__init__(message, *args, **kwargs) 20 | -------------------------------------------------------------------------------- /src/django_docutils/lib/__init__.py: -------------------------------------------------------------------------------- 1 | """Django Docutils library package for Docutils and Django.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/lib/directives/__init__.py: -------------------------------------------------------------------------------- 1 | """Douctils directives for django-docutils.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/lib/directives/code.py: -------------------------------------------------------------------------------- 1 | """Pygments docutils directive. 2 | 3 | The Pygments reStructuredText directive 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~. 5 | 6 | This fragment is a `Docutils `_ 0.5 directive that 7 | renders source code (to HTML only, currently) via Pygments. 8 | 9 | To use it, adjust the options below and copy the code into a module 10 | that you import on initialization. The code then automatically 11 | registers a ``sourcecode`` directive that you can use instead of 12 | normal code blocks like this: 13 | 14 | .. code-block:: rst 15 | 16 | .. sourcecode:: python 17 | 18 | My code goes here. 19 | 20 | If you want to have different code styles, e.g. one with line numbers 21 | and one without, add formatters with their names in the ``VARIANTS`` dict 22 | below. You can invoke them instead of the ``DEFAULT`` one by using a 23 | directive option: 24 | 25 | .. code-block:: rst 26 | 27 | .. sourcecode:: python 28 | :linenos: 29 | 30 | My code goes here. 31 | 32 | Look at the `directive documentation `_ 33 | to get all the gory details. 34 | 35 | :copyright: Copyright 2006-2015 by the Pygments team, see AUTHORS. 36 | :license: BSD, see LICENSE for details. 37 | """ 38 | 39 | from __future__ import annotations 40 | 41 | import re 42 | import typing as t 43 | 44 | from docutils import nodes 45 | from docutils.parsers.rst import Directive, directives 46 | from pygments import highlight 47 | from pygments.formatters.html import HtmlFormatter 48 | from pygments.lexers import get_lexer_by_name 49 | from pygments.lexers.special import TextLexer 50 | 51 | if t.TYPE_CHECKING: 52 | from collections.abc import Callable 53 | 54 | from pygments.formatter import Formatter 55 | 56 | 57 | def patch_bash_session_lexer() -> None: 58 | """Monkey patch Bash Session lexer to gobble up initial space after prompt.""" 59 | from pygments.lexers.shell import BashSessionLexer 60 | 61 | BashSessionLexer._ps1rgx = re.compile( 62 | r"^((?:(?:\[.*?\])|(?:\(\S+\))?(?:| |sh\S*?|\w+\S+[@:]\S+(?:\s+\S+)" 63 | r"?|\[\S+[@:][^\n]+\].+))\s*[$#%] )(.*\n?)", 64 | ) 65 | 66 | 67 | # Options 68 | # ~~~~~~~ 69 | 70 | #: Set to True if you want inline CSS styles instead of classes 71 | INLINESTYLES = False 72 | 73 | 74 | #: The default formatter 75 | DEFAULT = HtmlFormatter(cssclass="highlight code-block", noclasses=INLINESTYLES) 76 | 77 | #: Add name -> formatter pairs for every variant you want to use 78 | VARIANTS: dict[str, Formatter[str]] = { 79 | # 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True), 80 | } 81 | 82 | DEFAULT_OPTION_SPEC: dict[str, Callable[[str], t.Any]] = dict.fromkeys( 83 | VARIANTS, 84 | directives.flag, 85 | ) 86 | 87 | 88 | class CodeBlock(Directive): 89 | """Source code syntax highlighting.""" 90 | 91 | required_arguments = 1 92 | optional_arguments = 0 93 | final_argument_whitespace = True 94 | option_spec = DEFAULT_OPTION_SPEC 95 | has_content = True 96 | 97 | def run(self) -> list[nodes.Node]: 98 | """Directive run method for CodeBlock.""" 99 | self.assert_has_content() 100 | try: 101 | lexer_name = self.arguments[0] 102 | 103 | lexer = get_lexer_by_name(lexer_name) 104 | except ValueError: 105 | # no lexer found - use the text one instead of an exception 106 | lexer = TextLexer() 107 | # take an arbitrary option if more than one is given 108 | formatter = (self.options and VARIANTS[next(iter(self.options))]) or DEFAULT 109 | parsed = highlight("\n".join(self.content), lexer, formatter) 110 | return [nodes.raw("", parsed, format="html")] 111 | 112 | 113 | def register_pygments_directive(directive: str = "code-block") -> None: 114 | """Register pygments directive. 115 | 116 | Parameters 117 | ---------- 118 | directive : str 119 | directive name to register pygments to. 120 | 121 | Examples 122 | -------- 123 | If you wish to use (override) code-block (default), that means: 124 | 125 | .. code-block:: rst 126 | 127 | .. code-block:: 128 | 129 | // will be highlighted by pygments 130 | """ 131 | directives.register_directive(directive, CodeBlock) 132 | -------------------------------------------------------------------------------- /src/django_docutils/lib/directives/registry.py: -------------------------------------------------------------------------------- 1 | """Register douctils directives for django-docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | from django.utils.module_loading import import_string 6 | from docutils.parsers.rst import directives 7 | 8 | from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST 9 | 10 | 11 | def register_django_docutils_directives() -> None: 12 | """Register docutils directives for a django application. 13 | 14 | Examples 15 | -------- 16 | In your site's :ref:`Django settings module`: 17 | 18 | >>> DJANGO_DOCUTILS_LIB_RST = { 19 | ... 'directives': { #: directive-name: Directive class (import string) 20 | ... 'code-block': 'django_docutils.lib.directives.code.CodeBlock' 21 | ... } 22 | ... } 23 | """ 24 | if not DJANGO_DOCUTILS_LIB_RST: 25 | return 26 | 27 | if "directives" in DJANGO_DOCUTILS_LIB_RST: 28 | for dir_name, dir_cls_str in DJANGO_DOCUTILS_LIB_RST["directives"].items(): 29 | class_ = import_string(dir_cls_str) 30 | directives.register_directive(dir_name, class_) 31 | -------------------------------------------------------------------------------- /src/django_docutils/lib/metadata/__init__.py: -------------------------------------------------------------------------------- 1 | """Metadata extraction and processing for Django Docutils.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/lib/metadata/extract.py: -------------------------------------------------------------------------------- 1 | """Title, Subtitle, and Metadata extraction of reStructuredText.""" 2 | 3 | from __future__ import annotations 4 | 5 | from django.template.defaultfilters import truncatewords 6 | from django.utils.html import strip_tags 7 | from docutils import nodes 8 | 9 | 10 | def extract_title(document: nodes.document) -> str | None: 11 | """Return the title of the document. 12 | 13 | Parameters 14 | ---------- 15 | document : :class:`docutils.nodes.document` 16 | Document to return title for. 17 | 18 | Returns 19 | ------- 20 | str or None 21 | Title of document or None if not found. 22 | """ 23 | for node in document.traverse(nodes.PreBibliographic): # type:ignore 24 | if isinstance(node, nodes.title): 25 | return node.astext() 26 | return None 27 | 28 | 29 | def extract_metadata(document: nodes.document) -> dict[str, str]: 30 | """Return the dict containing document metadata. 31 | 32 | Parameters 33 | ---------- 34 | document : :class:`docutils.nodes.document` 35 | Document extract metadata from. 36 | 37 | Returns 38 | ------- 39 | dict 40 | docinfo data from document 41 | 42 | See Also 43 | -------- 44 | From: https://github.com/adieu/mezzanine-cli @ mezzanine_cli/parser.py 45 | License: BSD (https://github.com/adieu/mezzanine-cli/blob/master/setup.py) 46 | """ 47 | output = {} 48 | for docinfo in document.traverse(nodes.docinfo): 49 | for element in docinfo.children: 50 | if not isinstance(element, nodes.Text) and not isinstance( 51 | element, 52 | nodes.Element, 53 | ): 54 | continue 55 | 56 | if element.tagname == "field": # custom fields (e.g. summary) 57 | assert len(element.children) == 2 58 | name_elem, body_elem = element.children 59 | assert isinstance(name_elem, (nodes.Text, nodes.Element)) 60 | assert isinstance(body_elem, (nodes.Text, nodes.Element)) 61 | name = name_elem.astext() 62 | value = body_elem.astext() 63 | elif isinstance( 64 | element, 65 | (nodes.Text, nodes.TextElement), 66 | ): # standard fields (e.g. address) 67 | name = element.tagname 68 | value = element.astext() 69 | else: 70 | msg = f"No support for {element} of type {type(element)}" 71 | raise NotImplementedError( 72 | msg, 73 | ) 74 | name = name.lower() 75 | 76 | output[name] = value 77 | return output 78 | 79 | 80 | def extract_subtitle(document: nodes.document) -> str | None: 81 | """Return the subtitle of the document.""" 82 | for node in document.traverse(nodes.PreBibliographic): # type:ignore 83 | if isinstance(node, nodes.subtitle): 84 | return node.astext() 85 | return None 86 | 87 | 88 | def extract_abstract(doctree: nodes.document, length: int = 100) -> str: 89 | """Pull first n words from a docutils document. 90 | 91 | We use this to create snippets for Twitter Cards, FB, etc. 92 | 93 | Parameters 94 | ---------- 95 | doctree : :class:`docutils.nodes.document` 96 | Document to extract abstract from. 97 | length : int 98 | Word count to cut content off at. 99 | 100 | Returns 101 | ------- 102 | str 103 | truncated content, html tags removed 104 | """ 105 | paragraph_nodes = doctree.traverse(nodes.paragraph) 106 | text = "" 107 | for node in paragraph_nodes: 108 | text += node.astext() 109 | if len(text.split(" ")) > 100: 110 | break 111 | return truncatewords(strip_tags(text), 100) 112 | -------------------------------------------------------------------------------- /src/django_docutils/lib/metadata/process.py: -------------------------------------------------------------------------------- 1 | """Metadata is a catch-all term for information for an RST document. 2 | 3 | It can be pulled from the RST file itself, such as: 4 | 5 | - The Title/Subtitle of the document 6 | - The docinfo attributes 7 | 8 | In the case of directory-style projects, the manifest.json. 9 | 10 | These optional pipeline functions can be configured to to create, read, 11 | update, and delete metadata from RST projects. 12 | 13 | To set metadata processors, use DJANGO_DOCUTILS_LIB_RST['metadata_processors']:: 14 | 15 | DJANGO_DOCUTILS_LIB_RST = { 16 | 'metadata_processors': [ 17 | 'django_docutils.lib.metadata.processors.process_datetime' 18 | ], 19 | } 20 | 21 | The order of the processors will be respected. 22 | 23 | The function accepts one argument, the metadata dictionary, and returns the 24 | same dictionary:: 25 | 26 | def process_datetime(metadata): 27 | # create, read, update, delete metadata from the RST document 28 | return metadata 29 | 30 | See *processors.py* for more examples. 31 | """ 32 | 33 | from __future__ import annotations 34 | 35 | from django.utils.module_loading import import_string 36 | 37 | from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST 38 | 39 | 40 | def process_metadata(metadata: dict[str, str]) -> dict[str, str]: 41 | """Return objects from RST metadata pulled from document source. 42 | 43 | This will turn things like string dates into time-zone'd dateutil objects. 44 | 45 | Parameters 46 | ---------- 47 | metadata : dict 48 | Data returned from processing an RST file 49 | 50 | Returns 51 | ------- 52 | dict 53 | Metadata from rst file. 54 | """ 55 | if not DJANGO_DOCUTILS_LIB_RST: 56 | return metadata 57 | 58 | if "metadata_processors" in DJANGO_DOCUTILS_LIB_RST: 59 | for processor_str in DJANGO_DOCUTILS_LIB_RST["metadata_processors"]: 60 | processor_fn = import_string(processor_str) 61 | metadata = processor_fn(metadata) 62 | 63 | return metadata 64 | -------------------------------------------------------------------------------- /src/django_docutils/lib/metadata/processors.py: -------------------------------------------------------------------------------- 1 | """Metadata processing for Django Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import datetime 6 | import typing as t 7 | 8 | from django.conf import settings 9 | 10 | HAS_PYTZ = False 11 | try: 12 | import pytz 13 | 14 | HAS_PYTZ = True 15 | except ImportError: 16 | pass 17 | 18 | 19 | def process_datetime(metadata: dict[str, t.Any]) -> dict[str, t.Any]: 20 | """Optionally supports localizing times via pytz.""" 21 | timezone_formats = [ # timezone formats to try, most detailed to least 22 | "%Y-%m-%d %I:%M%p", 23 | "%Y-%m-%d", 24 | ] 25 | 26 | for time_key in ["created", "modified"]: 27 | if time_key in metadata: 28 | for _format in timezone_formats: 29 | try: 30 | metadata[time_key] = datetime.datetime.strptime( 31 | metadata[time_key], 32 | _format, 33 | ) 34 | break 35 | except ValueError: 36 | continue 37 | 38 | if HAS_PYTZ: 39 | metadata[time_key] = pytz.timezone(settings.TIME_ZONE).localize( 40 | metadata[time_key], 41 | is_dst=None, 42 | ) 43 | return metadata 44 | 45 | 46 | def process_anonymous_user(metadata: dict[str, t.Any]) -> dict[str, t.Any]: 47 | """Corrects name of author "anonymous" to django's anonymous username.""" 48 | if metadata.get("author") == "anonymous" and hasattr( 49 | settings, 50 | "DJANGO_DOCUTIL_ANONYMOUS_USER_NAME", 51 | ): 52 | metadata["author"] = settings.DJANGO_DOCUTILS_ANONYMOUS_USER_NAME 53 | 54 | return metadata 55 | -------------------------------------------------------------------------------- /src/django_docutils/lib/metadata/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Package to test docutils metadata processing for Django Docutils.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/lib/metadata/tests/test_extract.py: -------------------------------------------------------------------------------- 1 | """Test metadata and title extraction from reStructuredText.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django.utils.encoding import force_bytes 8 | from docutils.core import publish_doctree 9 | 10 | from django_docutils.lib.metadata.extract import ( 11 | extract_metadata, 12 | extract_subtitle, 13 | extract_title, 14 | ) 15 | from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST 16 | 17 | if t.TYPE_CHECKING: 18 | import pathlib 19 | 20 | 21 | def test_extract_title() -> None: 22 | """Assert title extraction from reStructuredText.""" 23 | content = """ 24 | =========== 25 | Hello world 26 | =========== 27 | 28 | :key1: value 29 | :key2: value 30 | 31 | more text 32 | 33 | first section 34 | ------------- 35 | 36 | some content 37 | """.strip() 38 | 39 | doctree = publish_doctree(source=force_bytes(content)) 40 | 41 | assert extract_title(doctree) == "Hello world" 42 | 43 | 44 | def test_extract_subtitle() -> None: 45 | """Assert subtitle extraction from reStructuredText.""" 46 | content = """ 47 | =========== 48 | Hello world 49 | =========== 50 | moo 51 | === 52 | 53 | :key1: value 54 | :key2: value 55 | 56 | more text 57 | 58 | first section 59 | ------------- 60 | 61 | some content 62 | """.strip() 63 | 64 | doctree = publish_doctree(source=force_bytes(content)) 65 | 66 | assert extract_subtitle(doctree) == "moo" 67 | 68 | 69 | def test_extract_metadata(tmp_path: pathlib.Path) -> None: 70 | """Assert metadata extraction from reStructuredText.""" 71 | docutils_settings = DJANGO_DOCUTILS_LIB_RST.get("docutils", {}) 72 | content = """ 73 | =========== 74 | Content ok! 75 | =========== 76 | 77 | :programming_languages: javascript 78 | :topics: webpack 79 | :Created: 2017-07-30 80 | :Author: tony 81 | 82 | more text 83 | 84 | first section 85 | ------------- 86 | 87 | some content 88 | """.strip() 89 | 90 | doctree = publish_doctree( 91 | source=force_bytes(content), 92 | settings_overrides=docutils_settings, 93 | ) 94 | 95 | assert extract_metadata(doctree) == { 96 | "programming_languages": "javascript", 97 | "topics": "webpack", 98 | "created": "2017-07-30", 99 | "author": "tony", 100 | } 101 | -------------------------------------------------------------------------------- /src/django_docutils/lib/metadata/tests/test_process.py: -------------------------------------------------------------------------------- 1 | """Test docutils metadata (docinfo) processing in Django Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import datetime 6 | 7 | from django.utils.encoding import force_bytes 8 | from docutils.core import publish_doctree 9 | 10 | from django_docutils.lib.metadata.extract import extract_metadata 11 | from django_docutils.lib.metadata.process import process_metadata 12 | 13 | 14 | def test_process_metadata_file() -> None: 15 | """Assert metadata extracted from reStructuredText docinfo.""" 16 | source = """ 17 | =========== 18 | Content ok! 19 | =========== 20 | 21 | :programming_language: javascript 22 | :topic: webpack 23 | :Created: 2017-07-30 24 | :Author: tony 25 | 26 | more text 27 | 28 | first section 29 | ------------- 30 | 31 | some content 32 | """.strip() 33 | 34 | doctree = publish_doctree(source=force_bytes(source)) 35 | 36 | raw_metadata = extract_metadata(doctree) 37 | 38 | analyzed_metadata = process_metadata(raw_metadata.copy()) 39 | assert set(raw_metadata.keys()) == set(analyzed_metadata.keys()) 40 | 41 | assert isinstance(analyzed_metadata["created"], datetime.date) 42 | 43 | assert process_metadata(raw_metadata) != { 44 | "programming_languages": "javascript", 45 | "topics": "webpack", 46 | "created": "2017-07-30", 47 | "author": "tony", 48 | } 49 | 50 | 51 | def test_process_metadata_daytime_timezone() -> None: 52 | """Verify time of day and timezone (optional) work with dates.""" 53 | source = """ 54 | =========== 55 | Content ok! 56 | =========== 57 | 58 | :programming_language: javascript 59 | :topic: webpack 60 | :Created: 2017-07-30 2:30PM 61 | :Author: tony 62 | 63 | more text 64 | 65 | first section 66 | ------------- 67 | 68 | some content 69 | """.strip() 70 | 71 | doctree = publish_doctree(source=force_bytes(source)) 72 | 73 | raw_metadata = extract_metadata(doctree) 74 | 75 | analyzed_metadata = process_metadata(raw_metadata.copy()) 76 | assert set(raw_metadata.keys()) == set(analyzed_metadata.keys()) 77 | 78 | created = analyzed_metadata["created"] 79 | 80 | assert isinstance(created, datetime.datetime) 81 | assert created.year == 2017 82 | assert created.month == 7 83 | assert created.day == 30 84 | assert created.strftime("%I") == "02" 85 | assert created.strftime("%p") == "PM" 86 | assert created.minute == 30 87 | -------------------------------------------------------------------------------- /src/django_docutils/lib/publisher.py: -------------------------------------------------------------------------------- 1 | """Docutils Publisher fors for Django Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django.utils.encoding import force_bytes, force_str 8 | from django.utils.safestring import mark_safe 9 | from docutils import io, nodes 10 | from docutils.core import Publisher, publish_doctree as docutils_publish_doctree 11 | from docutils.readers.doctree import Reader 12 | 13 | from .directives.registry import register_django_docutils_directives 14 | from .roles.registry import register_django_docutils_roles 15 | from .settings import DJANGO_DOCUTILS_LIB_RST 16 | from .transforms.toc import Contents 17 | from .writers import DjangoDocutilsWriter 18 | 19 | if t.TYPE_CHECKING: 20 | from docutils import SettingsSpec 21 | from docutils.writers.html5_polyglot import Writer 22 | 23 | docutils_settings = DJANGO_DOCUTILS_LIB_RST.get("docutils", {}) 24 | 25 | 26 | def publish_parts_from_doctree( 27 | document: nodes.document, 28 | destination_path: str | None = None, 29 | writer: Writer | None = None, 30 | writer_name: str = "pseudoxml", 31 | settings: t.Any | None = None, 32 | settings_spec: SettingsSpec | None = None, 33 | settings_overrides: t.Any | None = None, 34 | config_section: str | None = None, 35 | enable_exit_status: bool = False, 36 | ) -> dict[str, str]: 37 | """Render docutils doctree into docutils parts.""" 38 | reader = Reader(parser_name="null") # type:ignore 39 | pub = Publisher( 40 | reader, 41 | None, 42 | writer, 43 | source=io.DocTreeInput(document), 44 | destination_class=io.StringOutput, 45 | settings=settings, 46 | ) 47 | if not writer and writer_name: 48 | pub.set_writer(writer_name) 49 | pub.process_programmatic_settings( 50 | settings_spec, 51 | settings_overrides, 52 | config_section, 53 | ) 54 | pub.set_destination(None, destination_path) 55 | pub.publish(enable_exit_status=enable_exit_status) 56 | return pub.writer.parts # type:ignore 57 | 58 | 59 | def publish_toc_from_doctree( 60 | doctree: nodes.document, 61 | writer: Writer | None = None, 62 | ) -> str | None: 63 | """Publish table of contents from docutils doctree.""" 64 | if not writer: 65 | writer = DjangoDocutilsWriter() 66 | # Create a new document tree with just the table of contents 67 | # ========================================================== 68 | 69 | # document tree template: 70 | toc_tree = nodes.document( 71 | "", # type:ignore 72 | "", # type:ignore 73 | source="toc-generator", 74 | classes=["fixed-toc-menu menu"], 75 | ) 76 | toc_tree += nodes.paragraph("", "Contents", classes=["menu-label"]) 77 | # Set up a Contents instance: 78 | # The Contents transform requires a "pending" startnode and 79 | # generation options startnode 80 | pending = nodes.pending(Contents, rawsource="") # type:ignore 81 | 82 | contents_transform = Contents(doctree, pending) 83 | 84 | # this assures we get backlinks pointing to themselves 85 | # so users can copy anchor from headers 86 | contents_transform.backlinks = "entry" 87 | 88 | toc_contents = contents_transform.build_contents(doctree) 89 | 90 | if not toc_contents: # ToC is empty 91 | return None 92 | 93 | # run the contents builder and append the result to the template: 94 | toc_topic = nodes.topic(classes=["contents", "toc"]) 95 | 96 | toc_topic += toc_contents 97 | toc_tree += toc_topic 98 | toc = publish_parts_from_doctree(toc_tree, writer=writer) 99 | return mark_safe(force_str(toc["html_body"])) 100 | 101 | 102 | def publish_doctree( 103 | source: str | bytes, 104 | settings_overrides: t.Any = docutils_settings, 105 | ) -> nodes.document: 106 | """Split off ability to get doctree (a.k.a. document). 107 | 108 | It's valuable to be able to run transforms to alter and most importantly, 109 | extract data like post abstracts. 110 | 111 | Parameters 112 | ---------- 113 | source : str or bytes 114 | RST content 115 | settings_overrides : dict 116 | Settings overrides for docutils 117 | 118 | Returns 119 | ------- 120 | docutils.nodes.document 121 | document/doctree for reStructuredText content 122 | """ 123 | register_django_docutils_directives() 124 | register_django_docutils_roles() 125 | 126 | return docutils_publish_doctree( # type:ignore 127 | source=force_bytes(source), 128 | settings_overrides=settings_overrides, 129 | ) 130 | 131 | 132 | if t.TYPE_CHECKING: 133 | from typing_extensions import NotRequired, TypedDict, Unpack 134 | 135 | class PublishHtmlDocTreeKwargs(TypedDict): 136 | """Keyword arguments accepted by publish_html_from_source.""" 137 | 138 | show_title: NotRequired[bool] 139 | toc_only: NotRequired[bool] 140 | 141 | 142 | def publish_html_from_source( 143 | source: str, 144 | **kwargs: Unpack[PublishHtmlDocTreeKwargs], 145 | ) -> str | None: 146 | """Return HTML from reStructuredText source string.""" 147 | doctree = publish_doctree(source) 148 | return publish_html_from_doctree(doctree, **kwargs) 149 | 150 | 151 | def publish_html_from_doctree( 152 | doctree: nodes.document, 153 | show_title: bool = True, 154 | toc_only: bool = False, 155 | ) -> str | None: 156 | """Return HTML from reStructuredText document (doctree). 157 | 158 | Parameters 159 | ---------- 160 | value : str 161 | Contents from template being placed into node 162 | show_title : bool 163 | Show top level title 164 | toc_only : bool 165 | Special flag: return show TOC, used for sidebars 166 | 167 | Returns 168 | ------- 169 | str or None 170 | HTML from reStructuredText document (doctree) 171 | """ 172 | writer = DjangoDocutilsWriter() 173 | 174 | doctree.transformer.apply_transforms() 175 | 176 | if toc_only: # special flag to only return toc, used for sidebars 177 | return publish_toc_from_doctree(doctree, writer=writer) 178 | 179 | parts = publish_parts_from_doctree( 180 | doctree, 181 | writer=writer, 182 | settings_overrides=docutils_settings, 183 | ) 184 | 185 | if show_title: 186 | return mark_safe(force_str(parts["html_body"])) 187 | return mark_safe(force_str(parts["fragment"])) 188 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/__init__.py: -------------------------------------------------------------------------------- 1 | """Docutils roles for django-docutils.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/common.py: -------------------------------------------------------------------------------- 1 | """Core parts for Django Docutils roles.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from docutils import nodes, utils 8 | 9 | from django_docutils.lib.utils import split_explicit_title 10 | 11 | if t.TYPE_CHECKING: 12 | from .types import RemoteUrlHandlerFn, RoleFnReturnValue, UrlHandlerFn 13 | 14 | 15 | def generic_url_role( 16 | name: str, 17 | text: str, 18 | url_handler_fn: UrlHandlerFn, 19 | innernodeclass: type[nodes.Text | nodes.TextElement] = nodes.Text, 20 | ) -> RoleFnReturnValue: 21 | """Docutils Role for Django Docutils. 22 | 23 | This generic role also handles explicit titles (``:role:`yata yata ```) 24 | 25 | This breaks convention a feels a bit jenky at first. It uses a callback 26 | because figuring out the url is the only magic that happens, but its 27 | sandwiched in the middle. 28 | 29 | Parameters 30 | ---------- 31 | name : name of the role, e.g. 'github' 32 | text : text inside of the role, e.g: 33 | - 'airline-mode/airline-mode' 34 | - 'this repo ' 35 | url_handler_fn : :data:`django_docutils.lib.roles.types.UrlHandlerFn` 36 | a function that accepts the target param 37 | 38 | Returns 39 | ------- 40 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 41 | 42 | Examples 43 | -------- 44 | .. code-block:: python 45 | 46 | def github_role( 47 | name, rawtext, text, lineno, inliner, options={}, content=[] 48 | ): 49 | def url_handler(target): 50 | return 'https://github.com/{}'.format(target) 51 | 52 | return generic_url_role(name, text, url_handler) 53 | 54 | roles.register_local_role('gh', github_role) 55 | """ 56 | name = name.lower() 57 | has_explicit_title, title, target = split_explicit_title(text) 58 | title = utils.unescape(title) 59 | target = utils.unescape(target) 60 | 61 | if not has_explicit_title: 62 | title = utils.unescape(title) 63 | elif title[:2] == "**" and title[-2:] == "**": 64 | innernodeclass = nodes.strong 65 | title = title.strip("**") # noqa: B005 66 | # In Python 3.9+ 67 | # title = title.removeprefix("**").removesuffix("**") 68 | elif title[0] == "*" and title[-1] == "*": 69 | innernodeclass = nodes.emphasis 70 | title = title.strip("*") 71 | 72 | url = url_handler_fn(target) 73 | 74 | sn = innernodeclass(title) 75 | rn = nodes.reference("", "", internal=True, refuri=url, classes=[name]) 76 | rn += sn 77 | return [rn], [] 78 | 79 | 80 | def generic_remote_url_role( 81 | name: str, 82 | text: str, 83 | url_handler_fn: RemoteUrlHandlerFn, 84 | innernodeclass: type[nodes.Text | nodes.TextElement] = nodes.Text, 85 | ) -> tuple[list[nodes.reference], list[t.Any]]: 86 | """Docutils Role that can call an external data source for title and URL. 87 | 88 | Same as generic_url_role, but can return url and title via external data source. 89 | 90 | The ``url_handler_fn`` returns a title and a url. 91 | 92 | In cases like Amazon API, database lookups, and other stuff, information 93 | may be looked up by key, and we may get a fresh title to fill in if nothing 94 | else explicit is mentioned. 95 | 96 | Parameters 97 | ---------- 98 | name : name of the role, e.g. 'github' 99 | text : text inside of the role, e.g: 100 | - 'airline-mode/airline-mode' 101 | - 'this repo ' 102 | url_handler_fn : :data:`django_docutils.lib.roles.types.RemoteUrlHandlerFn` 103 | a function that accepts the target param, example: 104 | 105 | Returns 106 | ------- 107 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 108 | 109 | Examples 110 | -------- 111 | Simple example, let's create a role: 112 | 113 | .. code-block:: python 114 | 115 | def amzn_role( 116 | name, rawtext, text, lineno, inliner, options={}, content=[] 117 | ): 118 | def url_handler(target): 119 | query = amzn.lookup(ItemId=target) 120 | return query.title, query.offer_url 121 | 122 | return generic_remote_url_role(name, text, url_handler) 123 | 124 | roles.register_local_role('amzn', amzn_role) 125 | """ 126 | name = name.lower() 127 | has_explicit_title, title, target = split_explicit_title(text) 128 | title = utils.unescape(title) 129 | target = utils.unescape(target) 130 | 131 | remote_title, url = url_handler_fn(target) 132 | if not has_explicit_title: 133 | title = utils.unescape(remote_title) 134 | 135 | sn = innernodeclass(title) 136 | rn = nodes.reference("", "", internal=True, refuri=url, classes=[name]) 137 | rn += sn 138 | return [rn], [] 139 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/email.py: -------------------------------------------------------------------------------- 1 | """Email role for docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from .common import generic_url_role 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | from .types import RoleFnReturnValue 13 | 14 | 15 | def email_role( 16 | name: str, 17 | rawtext: str, 18 | text: str, 19 | lineno: int, 20 | inliner: Inliner, 21 | options: dict[str, t.Any] | None = None, 22 | content: str | None = None, 23 | ) -> RoleFnReturnValue: 24 | """Role for linking to email articles. 25 | 26 | Returns 27 | ------- 28 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 29 | 30 | Examples 31 | -------- 32 | `me@localhost `_: 33 | 34 | .. code-block:: rst 35 | 36 | :email:`me@localhost` 37 | 38 | `Email me `_: 39 | 40 | .. code-block:: rst 41 | 42 | :email:`Email me ` 43 | """ 44 | if options is None: 45 | options = {} 46 | 47 | def url_handler(target: str) -> str: 48 | return f"mailto:{target}" 49 | 50 | return generic_url_role(name, text, url_handler) 51 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/file.py: -------------------------------------------------------------------------------- 1 | """File role for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import typing as t 7 | 8 | from docutils import nodes, utils 9 | 10 | if t.TYPE_CHECKING: 11 | from docutils.parsers.rst.states import Inliner 12 | 13 | 14 | def file_role( 15 | name: str, 16 | rawtext: str, 17 | text: str, 18 | lineno: int, 19 | inliner: Inliner, 20 | options: dict[str, t.Any] | None = None, 21 | content: str | None = None, 22 | ) -> tuple[list[nodes.emphasis], list[t.Any]]: 23 | """Role for files. 24 | 25 | Examples 26 | -------- 27 | *📄 ./path/to/moo*: 28 | 29 | .. code-block:: rst 30 | 31 | :file:`./path/to/moo` 32 | 33 | *📁 ./path/to/moo/*: 34 | 35 | .. code-block:: rst 36 | 37 | :file:`./path/to/moo/` 38 | """ 39 | if options is None: 40 | options = {} 41 | name = name.lower() 42 | title = utils.unescape(text) 43 | 44 | # 'file' would collide with bulma, so we use 'filepath' 45 | # https://github.com/jgthms/bulma/blob/c2fae71/sass/elements/form.sass#L218 46 | # https://github.com/jgthms/bulma/issues/1442 47 | classes = [] 48 | 49 | # add .fa class since this isn't a link 50 | classes.append("far") 51 | 52 | if title.endswith("/"): 53 | classes.append("fa-folder") 54 | else: 55 | classes.append("fa-file-alt") 56 | extension = os.path.splitext(title)[1] 57 | if extension: 58 | classes.append(extension.lstrip(".")) 59 | 60 | sn = nodes.emphasis(title, title) 61 | 62 | # insert inside the 63 | sn.insert(0, nodes.inline("", "", classes=classes)) 64 | return [sn], [] 65 | 66 | 67 | def manifest_role( 68 | name: str, 69 | rawtext: str, 70 | text: str, 71 | lineno: int, 72 | inliner: Inliner, 73 | options: dict[str, t.Any] | None = None, 74 | content: str | None = None, 75 | ) -> tuple[list[nodes.emphasis], list[t.Any]]: 76 | """Role for manifests (package.json, file outputs). 77 | 78 | Examples 79 | -------- 80 | *📄 package.json*: 81 | 82 | .. code-block:: rst 83 | 84 | :manifest:`package.json` 85 | 86 | """ 87 | if options is None: 88 | options = {} 89 | name = name.lower() 90 | title = utils.unescape(text) 91 | 92 | classes = ["manifest"] 93 | 94 | # add .fa class since this isn't a link 95 | classes.append("fa-file-alt far") 96 | 97 | sn = nodes.emphasis(title, title) 98 | 99 | # insert inside the 100 | sn.insert(0, nodes.inline("", "", classes=classes)) 101 | return [sn], [] 102 | 103 | 104 | def exe_role( 105 | name: str, 106 | rawtext: str, 107 | text: str, 108 | lineno: int, 109 | inliner: Inliner, 110 | options: dict[str, t.Any] | None = None, 111 | content: str | None = None, 112 | ) -> tuple[list[nodes.emphasis], list[t.Any]]: 113 | """Role for executables. 114 | 115 | Examples 116 | -------- 117 | *📄 ./path/to/webpack*: 118 | 119 | .. code-block:: rst 120 | 121 | :exe:`./path/to/webpack` 122 | """ 123 | if options is None: 124 | options = {} 125 | name = name.lower() 126 | title = utils.unescape(text) 127 | 128 | classes = ["exe", "fa"] 129 | 130 | sn = nodes.emphasis(title, title) 131 | 132 | # insert inside the 133 | sn.insert(0, nodes.inline("", "", classes=classes)) 134 | return [sn], [] 135 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/github.py: -------------------------------------------------------------------------------- 1 | """GitHub role for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from .common import generic_url_role 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | from .types import RoleFnReturnValue 13 | 14 | 15 | def github_role( 16 | name: str, 17 | rawtext: str, 18 | text: str, 19 | lineno: int, 20 | inliner: Inliner, 21 | options: dict[str, t.Any] | None = None, 22 | content: str | None = None, 23 | ) -> RoleFnReturnValue: 24 | """Role for linking to GitHub repos and issues. 25 | 26 | Returns 27 | ------- 28 | RoleFnReturnValue 29 | 30 | Examples 31 | -------- 32 | `vim-airline `_: 33 | 34 | .. code-block:: rst 35 | 36 | :gh:`vim-airline` 37 | 38 | `vim-airline's org `_: 39 | 40 | .. code-block:: rst 41 | 42 | :gh:`vim-airline's org ` 43 | 44 | `vim-airline/vim-airline `_: 45 | 46 | .. code-block:: rst 47 | 48 | :gh:`vim-airline/vim-airline` 49 | 50 | `vim-airline/vim-airline#125 `_: 51 | 52 | .. code-block:: rst 53 | 54 | :gh:`vim-airline/vim-airline#125` 55 | 56 | `this example issue `_: 57 | 58 | .. code-block:: rst 59 | 60 | :gh:`this example issue ` 61 | """ 62 | if options is None: 63 | options = {} 64 | 65 | def url_handler(target: str) -> str: 66 | if "#" in target: 67 | user_n_repo, issue = target.split("#") 68 | if issue.isnumeric(): 69 | return f"https://github.com/{user_n_repo}/issue/{issue}" 70 | 71 | return f"https://github.com/{target}" 72 | 73 | return generic_url_role(name, text, url_handler) 74 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/hackernews.py: -------------------------------------------------------------------------------- 1 | """HN (HackerNews) role for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | from urllib.parse import quote 7 | 8 | from .common import generic_url_role 9 | 10 | if t.TYPE_CHECKING: 11 | from docutils.parsers.rst.states import Inliner 12 | 13 | from .types import RoleFnReturnValue 14 | 15 | 16 | def hackernews_role( 17 | name: str, 18 | rawtext: str, 19 | text: str, 20 | lineno: int, 21 | inliner: Inliner, 22 | options: dict[str, t.Any] | None = None, 23 | content: str | None = None, 24 | ) -> RoleFnReturnValue: 25 | """Role for linking to hackernews articles. 26 | 27 | Returns 28 | ------- 29 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 30 | 31 | Examples 32 | -------- 33 | `15610489 `_: 34 | 35 | .. code-block:: rst 36 | 37 | :hn:`15610489` 38 | 39 | `this hackernews article `_: 40 | 41 | .. code-block:: rst 42 | 43 | :hn:`this hackernews article <15610489>` 44 | """ 45 | if options is None: 46 | options = {} 47 | 48 | def url_handler(target: str) -> str: 49 | target = quote(target.replace(" ", "_")) 50 | return f"https://news.ycombinator.com/item?id={target}" 51 | 52 | return generic_url_role(name, text, url_handler) 53 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/kbd.py: -------------------------------------------------------------------------------- 1 | """ (Keyboard Input Element) role for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from docutils import nodes 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | 13 | def kbd_role( 14 | name: str, 15 | rawtext: str, 16 | text: str, 17 | lineno: int, 18 | inliner: Inliner, 19 | options: dict[str, t.Any] | None = None, 20 | content: str | None = None, 21 | ) -> tuple[list[nodes.raw], list[t.Any]]: 22 | """Role for ````, the keyboard input element. 23 | 24 | Examples 25 | -------- 26 | :kbd:`ctrl-t` 27 | 28 | .. code-block:: rst 29 | 30 | :kbd:`ctrl-t` 31 | """ 32 | html = "" 33 | keys = text.split(",") 34 | 35 | if isinstance(keys, str): 36 | keys = [keys] 37 | 38 | for key in keys: 39 | html += f"{key}" 40 | 41 | return [nodes.raw("", html, format="html")], [] 42 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/leanpub.py: -------------------------------------------------------------------------------- 1 | """Leanpub role for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from .common import generic_url_role 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | from .types import RoleFnReturnValue 13 | 14 | 15 | def leanpub_role( 16 | name: str, 17 | rawtext: str, 18 | text: str, 19 | lineno: int, 20 | inliner: Inliner, 21 | options: dict[str, t.Any] | None = None, 22 | content: str | None = None, 23 | ) -> RoleFnReturnValue: 24 | """Role for linking to leanpub page. 25 | 26 | Returns 27 | ------- 28 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 29 | 30 | Examples 31 | -------- 32 | `the-tao-of-tmux `_: 33 | 34 | .. code-block:: rst 35 | 36 | :leanpub:`the-tao-of-tmux` 37 | 38 | `my book `_: 39 | 40 | .. code-block:: rst 41 | 42 | :leanpub:`my book ` 43 | 44 | `The Tao of tmux `_: 45 | 46 | .. code-block:: rst 47 | 48 | :leanpub:`The Tao of tmux ` 49 | """ 50 | if options is None: 51 | options = {} 52 | 53 | def url_handler(target: str) -> str: 54 | if ":" in target: 55 | project, path = target.split(":") 56 | return f"https://leanpub.com/{project}/{path}" 57 | return f"https://leanpub.com/{target}" 58 | 59 | return generic_url_role(name, text, url_handler) 60 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/pypi.py: -------------------------------------------------------------------------------- 1 | """PyPI (Python Package Index) role for docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from .common import generic_url_role 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | from .types import RoleFnReturnValue 13 | 14 | 15 | def pypi_role( 16 | name: str, 17 | rawtext: str, 18 | text: str, 19 | lineno: int, 20 | inliner: Inliner, 21 | options: dict[str, t.Any] | None = None, 22 | content: str | None = None, 23 | ) -> RoleFnReturnValue: 24 | """Role for linking to PyPI (Python Package Index) page. 25 | 26 | Returns 27 | ------- 28 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 29 | 30 | Examples 31 | -------- 32 | `libsass `_: 33 | 34 | .. code-block:: rst 35 | 36 | :pypi:`libsass` 37 | 38 | `a pypi package `_: 39 | 40 | .. code-block:: rst 41 | 42 | :pypi:`a pypi package ` 43 | """ 44 | if options is None: 45 | options = {} 46 | 47 | def url_handler(target: str) -> str: 48 | return f"https://pypi.python.org/pypi/{target}" 49 | 50 | return generic_url_role(name, text, url_handler) 51 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/readthedocs.py: -------------------------------------------------------------------------------- 1 | """ReadTheDocs role for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from .common import generic_url_role 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | from .types import RoleFnReturnValue 13 | 14 | 15 | def readthedocs_role( 16 | name: str, 17 | rawtext: str, 18 | text: str, 19 | lineno: int, 20 | inliner: Inliner, 21 | options: dict[str, t.Any] | None = None, 22 | content: str | None = None, 23 | ) -> RoleFnReturnValue: 24 | """Role for linking to readthedocs.org page. 25 | 26 | Returns 27 | ------- 28 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 29 | 30 | Examples 31 | -------- 32 | `django-pipeline `_: 33 | 34 | .. code-block:: rst 35 | 36 | :rtd:`django-pipeline` 37 | 38 | `a rtd site `_: 39 | 40 | .. code-block:: rst 41 | 42 | :rtd:`a rtd site ` 43 | 44 | `python-guide:dev/virtualenvs `_: 45 | 46 | .. code-block:: rst 47 | 48 | :rtd:`python-guide:dev/virtualenvs` 49 | 50 | `about virtualenvs `_: 51 | 52 | .. code-block:: rst 53 | 54 | :rtd:`about virtualenvs ` 55 | """ 56 | if options is None: 57 | options = {} 58 | 59 | def url_handler(target: str) -> str: 60 | if ":" in target: 61 | project, path = target.split(":") 62 | return f"https://{project}.readthedocs.io/en/latest/{path}" 63 | return f"https://{target}.readthedocs.io" 64 | 65 | return generic_url_role(name, text, url_handler) 66 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/registry.py: -------------------------------------------------------------------------------- 1 | """Register docutils roles for django-docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import inspect 6 | import typing as t 7 | 8 | from django.utils.module_loading import import_string 9 | from docutils.parsers.rst import roles 10 | 11 | from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST 12 | 13 | 14 | def register_django_docutils_roles() -> None: 15 | """Register docutils roles for a django application. 16 | 17 | Examples 18 | -------- 19 | In your site's :ref:`Django settings module`: 20 | 21 | >>> DJANGO_DOCUTILS_LIB_RST = { 22 | ... #: directive-name: Directive class (import string) 23 | ... 'roles': { 24 | ... 'local': { #: roles.register_local_role 25 | ... # below: same as 26 | ... # roles.register_local_role('gh', github_role) 27 | ... 'gh': 'django_docutils.lib.roles.github.git_role', 28 | ... 'pypi': 'django_docutils.lib.roles.pypi.pypi_role', 29 | ... }, 30 | ... 'canonical': { #: roles.register_canonical_role 31 | ... # below: same as: 32 | ... # roles.register_canonical_role('class', PyXRefRole()) 33 | ... 'class': 'django_docutils.lib.roles.xref.PyXRefRole', 34 | ... # below: same as 35 | ... # roles.register_canonical_role( 36 | ... # 'ref', 37 | ... # XRefRole( 38 | ... # lowercase=True, innernodeclass=nodes.inline, 39 | ... # warn_dangling=True 40 | ... # ) 41 | ... # ) 42 | ... # See nodes.inline will be resolved 43 | ... 'ref': ( 44 | ... 'django_docutils.lib.roles.xref.XRefRole', 45 | ... { 46 | ... 'lowercase': True, 47 | ... 'innernodeclass': 'docutils.nodes.inline', 48 | ... 'warn_dangling': True 49 | ... } 50 | ... ), 51 | ... 'meth': ( 52 | ... 'django_docutils.lib.roles.xref.PyXRefRole', 53 | ... { 54 | ... 'fix_parens': True, 55 | ... }, 56 | ... ), 57 | ... }, 58 | ... }, 59 | ... } 60 | ... 61 | 62 | Returns 63 | ------- 64 | None 65 | """ 66 | if not DJANGO_DOCUTILS_LIB_RST: 67 | return 68 | 69 | if "roles" not in DJANGO_DOCUTILS_LIB_RST: 70 | return 71 | 72 | django_docutils_roles = DJANGO_DOCUTILS_LIB_RST["roles"] 73 | 74 | local_roles = django_docutils_roles.get("local", None) 75 | 76 | if local_roles: 77 | register_role_mapping(local_roles) 78 | 79 | return 80 | 81 | 82 | def register_role_mapping(role_mapping: dict[str, t.Any]) -> None: 83 | """Register a dict mapping of roles. 84 | 85 | An item consists of a role name, import string to a callable, and an 86 | optional mapping of keyword args for special roles that are classes 87 | that can accept arguments. 88 | 89 | The term inside 'cb' is short for callback/callable. Since the string can 90 | be any callable object: a function or class. 91 | 92 | Parameters 93 | ---------- 94 | role_mapping : dict 95 | Mapping of docutils roles to register 96 | """ 97 | for role_name, role_cb_str in role_mapping.items(): 98 | role_cb_kwargs = {} 99 | 100 | if isinstance(role_cb_str, tuple): 101 | # ( 102 | # 'django_docutils.lib.roles.xref.PyXRefRole', # role_cb_str 103 | # { # role_cb_kwargs 104 | # 'fix_parens': True 105 | # } 106 | # ), 107 | 108 | # pop off dict of kwargs 109 | role_cb_kwargs = role_cb_str[1] 110 | 111 | # move class string item to a pure string 112 | role_cb_str = role_cb_str[0] 113 | 114 | # One more check, we may have an innernodeclass that needs 115 | # to be resolved, e.g. 116 | # ( 117 | # 'django_docutils.lib.roles.xref.XRefRole', 118 | # { 119 | # 'lowercase': True, 120 | # 'innernodeclass': 'docutils.nodes.inline', 121 | # 'warn_dangling': True 122 | # } 123 | # ), 124 | if "innernodeclass" in role_cb_kwargs and isinstance( 125 | role_cb_kwargs["innernodeclass"], 126 | str, 127 | ): 128 | role_cb_kwargs["innernodeclass"] = import_string( 129 | role_cb_kwargs["innernodeclass"], 130 | ) 131 | 132 | # Docutils roles accept a function or callable class as a callback 133 | role_ = import_string(role_cb_str) 134 | 135 | # Stuff like cross-reference roles, which are derived from sphinx work 136 | # differently. Unlike normal function roles, these roles are classes 137 | # passed in instantiated. 138 | # 139 | # If they include kwargs, they are entered as a tuple with a second 140 | # element that's a dict of the kwargs passed into the role. 141 | if inspect.isclass(role_): 142 | if role_cb_kwargs: 143 | roles.register_local_role(role_name, role_(**role_cb_kwargs)) 144 | else: 145 | roles.register_local_role(role_name, role_()) 146 | else: 147 | roles.register_local_role(role_name, role_) 148 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/twitter.py: -------------------------------------------------------------------------------- 1 | """Twitter role for docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from .common import generic_url_role 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | from .types import RoleFnReturnValue 13 | 14 | 15 | def twitter_role( 16 | name: str, 17 | rawtext: str, 18 | text: str, 19 | lineno: int, 20 | inliner: Inliner, 21 | options: dict[str, t.Any] | None = None, 22 | content: str | None = None, 23 | ) -> RoleFnReturnValue: 24 | """Role for linking to twitter articles. 25 | 26 | Returns 27 | ------- 28 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 29 | 30 | Examples 31 | -------- 32 | `@username `_: 33 | 34 | .. code-block:: rst 35 | 36 | :twitter:`@username` 37 | 38 | `follow me on twitter `_: 39 | 40 | .. code-block:: rst 41 | 42 | :twitter:`follow me on twitter <@username>` 43 | """ 44 | if options is None: 45 | options = {} 46 | 47 | def url_handler(target: str) -> str: 48 | if "@" in target: 49 | target = target.replace("@", "") 50 | 51 | return f"https://twitter.com/{target}" 52 | 53 | return generic_url_role(name, text, url_handler) 54 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/types.py: -------------------------------------------------------------------------------- 1 | """Typings for Django Docutils roles for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from docutils import nodes 8 | from typing_extensions import Protocol 9 | 10 | 11 | class UrlHandlerFn(Protocol): 12 | """Protocol for role handler callback maps directly to a URL patern.""" 13 | 14 | def __call__(self, target: str) -> str: 15 | """Role function that directly maps to a URL.""" 16 | ... 17 | 18 | 19 | class RemoteUrlHandlerFn(Protocol): 20 | """Protocol for role handler callback that retrieve from external data sources.""" 21 | 22 | def __call__(self, target: str) -> tuple[str, str]: 23 | """Role function that can query an external source for its title.""" 24 | ... 25 | 26 | 27 | RoleFnReturnValue = tuple[list[nodes.reference], list[t.Any]] 28 | """Role function return value. 29 | 30 | See also 31 | -------- 32 | 33 | From `Docutils: How to: RST Roles 34 | `_: 35 | 36 | Role functions return a tuple of two values: 37 | 38 | - A list of nodes which will be inserted into the document tree at the point where 39 | the interpreted role was encountered (can be an empty list). 40 | - A list of system messages, which will be inserted into the document tree 41 | immediately after the end of the current block (can also be empty). 42 | """ 43 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/url.py: -------------------------------------------------------------------------------- 1 | """URL Role for docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from .common import generic_url_role 8 | 9 | if t.TYPE_CHECKING: 10 | from docutils.parsers.rst.states import Inliner 11 | 12 | from .types import RoleFnReturnValue 13 | 14 | 15 | def url_role( 16 | name: str, 17 | rawtext: str, 18 | text: str, 19 | lineno: int, 20 | inliner: Inliner, 21 | options: dict[str, t.Any] | None = None, 22 | content: str | None = None, 23 | ) -> RoleFnReturnValue: 24 | """Role for linking to url articles. 25 | 26 | Returns 27 | ------- 28 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 29 | 30 | Examples 31 | -------- 32 | https://google.com: 33 | 34 | .. code-block:: rst 35 | 36 | :url:`https://google.com` 37 | 38 | 39 | `Google `_: 40 | 41 | .. code-block:: rst 42 | 43 | :url:`Google ` 44 | 45 | |google|_: 46 | 47 | .. |google| replace:: *Google* 48 | 49 | .. _google: https://google.com 50 | 51 | .. code-block:: rst 52 | 53 | :url:`*Google* ` 54 | """ 55 | if options is None: 56 | options = {} 57 | 58 | def url_handler(target: str) -> str: 59 | return target 60 | 61 | return generic_url_role(name, text, url_handler) 62 | -------------------------------------------------------------------------------- /src/django_docutils/lib/roles/wikipedia.py: -------------------------------------------------------------------------------- 1 | """Wikipedia role for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | from urllib.parse import quote 7 | 8 | from .common import generic_url_role 9 | 10 | if t.TYPE_CHECKING: 11 | from docutils.parsers.rst.states import Inliner 12 | 13 | from .types import RoleFnReturnValue 14 | 15 | 16 | def wikipedia_role( 17 | name: str, 18 | rawtext: str, 19 | text: str, 20 | lineno: int, 21 | inliner: Inliner, 22 | options: dict[str, t.Any] | None = None, 23 | content: str | None = None, 24 | ) -> RoleFnReturnValue: 25 | """Role for linking to Wikipedia articles. 26 | 27 | Returns 28 | ------- 29 | :data:`django_docutils.lib.roles.types.RoleFnReturnValue` 30 | 31 | Examples 32 | -------- 33 | `Don't repeat yourself `_: 34 | 35 | .. code-block:: rst 36 | 37 | :wikipedia:`Don't repeat yourself` 38 | 39 | `this wikipedia article `_: 40 | 41 | .. code-block:: rst 42 | 43 | :wikipedia:`this wikipedia article ` 44 | """ 45 | if options is None: 46 | options = {} 47 | 48 | def url_handler(target: str) -> str: 49 | target = quote(target.replace(" ", "_")) 50 | return f"https://en.wikipedia.org/wiki/{target}" 51 | 52 | return generic_url_role(name, text, url_handler) 53 | -------------------------------------------------------------------------------- /src/django_docutils/lib/settings.py: -------------------------------------------------------------------------------- 1 | """Settings objects and type-mapping for Django Docutils library package.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django.conf import settings 8 | from django.core.signals import setting_changed 9 | 10 | if t.TYPE_CHECKING: 11 | from .types import DjangoDocutilsLibRSTSettings, DjangoDocutilsLibTextSettings 12 | 13 | DJANGO_DOCUTILS_LIB_RST = t.cast( 14 | "DjangoDocutilsLibRSTSettings", 15 | getattr(settings, "DJANGO_DOCUTILS_LIB_RST", {}), 16 | ) 17 | """Settings for reStructuredText""" 18 | 19 | DJANGO_DOCUTILS_LIB_TEXT = t.cast( 20 | "DjangoDocutilsLibTextSettings", 21 | getattr(settings, "DJANGO_DOCUTILS_LIB_TEXT", {"uncapitalized_word_filters": []}), 22 | ) 23 | 24 | DJANGO_DOCUTILS_ANONYMOUS_USER_NAME: str | None = "AnonymousCoward" 25 | 26 | 27 | def reload_settings( 28 | signal: t.Any, 29 | sender: t.Any, 30 | setting: str, 31 | value: t.Any, 32 | enter: bool, 33 | **kwargs: t.Any, 34 | ) -> None: 35 | """Ran when settings updated.""" 36 | if setting == "DJANGO_DOCUTILS_LIB_RST" and isinstance(value, dict): 37 | # mypy: See mypy#6262, mypy#9168. There's no equivalent to keyof in TypeScript 38 | DJANGO_DOCUTILS_LIB_RST.update(**value) # type:ignore 39 | 40 | # Register any added docutils roles or directives 41 | from django_docutils.lib.directives.registry import ( 42 | register_django_docutils_directives, 43 | ) 44 | from django_docutils.lib.roles.registry import register_django_docutils_roles 45 | 46 | register_django_docutils_roles() 47 | register_django_docutils_directives() 48 | 49 | 50 | setting_changed.connect(reload_settings) 51 | -------------------------------------------------------------------------------- /src/django_docutils/lib/templates/rst/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content_inner %} 4 |
5 | {{content}} 6 |
7 | {% endblock content_inner %} 8 | 9 | -------------------------------------------------------------------------------- /src/django_docutils/lib/templates/rst/raw.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load rst %} 3 | 4 | {% block content_inner %} 5 |
6 | {% restructuredtext content %} 7 |
8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /src/django_docutils/lib/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for Django Docutils library code.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/lib/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """Tests for Django Docutil utilities.""" 2 | 3 | from __future__ import annotations 4 | 5 | from django_docutils.lib.utils import chop_after_docinfo, chop_after_title 6 | 7 | 8 | def test_chop_after_title() -> None: 9 | """Assert reStructuredText content chopped after title.""" 10 | content = """============================================= 11 | Learn JavaScript for free: The best resources 12 | ============================================= 13 | 14 | first section 15 | ------------- 16 | 17 | some content 18 | """.strip() 19 | 20 | result = chop_after_title(content) 21 | 22 | expected = """ 23 | first section 24 | ------------- 25 | 26 | some content""".strip() 27 | 28 | assert result == expected 29 | 30 | 31 | def test_chop_after_docinfo() -> None: 32 | """Assert reStructuredText content chopped after docinfo.""" 33 | before = """ 34 | =========== 35 | Content ok! 36 | =========== 37 | 38 | :programming_languages: javascript 39 | :topics: webpack 40 | :Created: 2017-07-30 41 | :Author: tony 42 | 43 | more text 44 | 45 | first section 46 | ------------- 47 | 48 | some content 49 | """.strip() 50 | 51 | after = """ 52 | more text 53 | 54 | first section 55 | ------------- 56 | 57 | some content 58 | """.strip() 59 | 60 | assert chop_after_docinfo(before) == after 61 | 62 | # test docinfo handles spaces in values 63 | assert ( 64 | chop_after_docinfo( 65 | source=""" 66 | ============== 67 | Document title 68 | ============== 69 | ----------------- 70 | Document subtitle 71 | ----------------- 72 | 73 | :Title: Overridden Title 74 | :Subtitle: Overridden Subtitle 75 | 76 | Content 77 | ------- 78 | 79 | hi 80 | """.strip(), 81 | ) 82 | == """ 83 | Content 84 | ------- 85 | 86 | hi""".strip() 87 | ) 88 | -------------------------------------------------------------------------------- /src/django_docutils/lib/tests/test_writers.py: -------------------------------------------------------------------------------- 1 | """Tests for Django Docutils Writers.""" 2 | 3 | from __future__ import annotations 4 | 5 | from django.utils.encoding import force_bytes 6 | from docutils.core import publish_doctree 7 | from docutils.writers.html5_polyglot import Writer 8 | 9 | from django_docutils.lib.publisher import publish_parts_from_doctree 10 | from django_docutils.lib.settings import DJANGO_DOCUTILS_LIB_RST 11 | from django_docutils.lib.writers import DjangoDocutilsWriter 12 | 13 | 14 | def test_HTMLWriter_hides_docinfo() -> None: 15 | """Assert HTMLWriter hides docinfo.""" 16 | docutils_settings = DJANGO_DOCUTILS_LIB_RST.get("docutils", {}) 17 | 18 | content = """ 19 | =========== 20 | Hello world 21 | =========== 22 | 23 | :key1: value 24 | :key2: value 25 | 26 | more text 27 | 28 | first section 29 | ------------- 30 | 31 | some content 32 | """.strip() 33 | 34 | doctree = publish_doctree( 35 | source=force_bytes(content), 36 | settings_overrides=docutils_settings, 37 | ) 38 | 39 | # Test that normal writer will show docinfo in HTML 40 | parts = publish_parts_from_doctree( 41 | doctree, 42 | writer=Writer(), 43 | settings_overrides=docutils_settings, 44 | ) 45 | assert "key1" in parts["html_body"] 46 | 47 | # Our writer should *not* output docinto 48 | parts = publish_parts_from_doctree( 49 | doctree, 50 | writer=DjangoDocutilsWriter(), 51 | settings_overrides=docutils_settings, 52 | ) 53 | 54 | assert "key1" not in parts["html_body"] 55 | assert "first section" in parts["html_body"] 56 | -------------------------------------------------------------------------------- /src/django_docutils/lib/text.py: -------------------------------------------------------------------------------- 1 | """Text related utilities for Django Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | 7 | from django.conf import settings 8 | from django.utils.module_loading import import_string 9 | 10 | _word_re = re.compile(r"\w+", re.UNICODE) 11 | _word_beginning_split_re = re.compile(r"([\s\(\{\[\<]+)", re.UNICODE) 12 | 13 | 14 | def is_uncapitalized_word(value: str) -> bool: 15 | """Return True if term/word segment is special uncap term (e.g. "django-"). 16 | 17 | Parameters 18 | ---------- 19 | value : str 20 | string value from template 21 | 22 | Returns 23 | ------- 24 | bool 25 | True if term or word is uncapitalized. 26 | 27 | Functions can be declared via DJANGO_DOCUTILS_TEXT in django settings via string 28 | imports. The filters accept one argument (the word). If you don't want the 29 | word/pattern capitalized, return True. Anything else capitalizes as normal. 30 | 31 | How to create filters: 32 | 33 | .. code-block:: python 34 | 35 | def handle_uncapped_word(value: str) -> bool: 36 | if value.startswith('django-'): 37 | return True 38 | if 'vs' in value: 39 | return True 40 | return False 41 | 42 | In your settings: 43 | 44 | .. code-block:: python 45 | 46 | DJANGO_DOCUTILS_LIB_TEXT = { 47 | 'uncapitalized_word_filters': [ 48 | 'project.path.to.handle_uncapped_word' 49 | ] 50 | } 51 | """ 52 | try: 53 | config = settings.DJANGO_DOCUTILS_LIB_TEXT 54 | except AttributeError: 55 | return False 56 | 57 | if "uncapitalized_word_filters" in config: 58 | for filter_fn_str in config["uncapitalized_word_filters"]: 59 | filter_ = import_string(filter_fn_str) 60 | if filter_(value): 61 | return True 62 | return False 63 | 64 | 65 | def smart_capfirst(value: str) -> str: 66 | """Capitalize the first character of the value.""" 67 | return value[0].upper() + value[1:] 68 | 69 | 70 | def smart_title(value: str) -> str: 71 | """Convert a string into titlecase, except for special cases. 72 | 73 | Django can still be capitalized, but it must already be like that. 74 | """ 75 | return "".join( 76 | [ 77 | smart_capfirst(item) 78 | for item in _word_beginning_split_re.split(value) 79 | if item 80 | ], 81 | ) 82 | -------------------------------------------------------------------------------- /src/django_docutils/lib/transforms/__init__.py: -------------------------------------------------------------------------------- 1 | """Docutils transformers for Django Docutils.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/lib/transforms/code.py: -------------------------------------------------------------------------------- 1 | """Code related formatter and transformers.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | import typing as t 7 | 8 | from docutils import nodes 9 | from docutils.transforms import Transform 10 | from pygments import highlight 11 | from pygments.formatters.html import HtmlFormatter 12 | from pygments.token import Token 13 | 14 | if t.TYPE_CHECKING: 15 | from collections.abc import Iterator 16 | 17 | from pygments.token import _TokenType 18 | 19 | TokenStream = Iterator[tuple[_TokenType, str]] 20 | TokenGenerator = Iterator[tuple[t.Union[str, int], str]] 21 | 22 | 23 | class InlineHtmlFormatter(HtmlFormatter): # type:ignore 24 | """HTMLFormatter for inline codeblocks.""" 25 | 26 | def format_unencoded(self, tokensource: TokenStream, outfile: t.Any) -> None: 27 | r"""Trim inline element of space and newlines. 28 | 29 | 1. Trailing newline: Final token generated returns ``(Token.Other, '\n')`` 30 | results in a blank space ````. 31 | 32 | This would be unnoticeable for a code block, but as this is inline, it looks 33 | strange. Let's filter out any trailing newlines from token source, then 34 | fallback to the normal process by passing it back into parent class method. 35 | 2. Trailing space: 36 | 37 | *After* this method ``_format_lines`` still adds a ``\n`` (which renders as a 38 | space again in the browser). To suppress that pass ``lineseparator=''`` to 39 | the ``InlineHtmlFormatter`` class. 40 | """ 41 | 42 | def filter_trailing_newline(source: TokenStream) -> TokenStream: 43 | tokens = list(source) 44 | 45 | # filter out the trailing newline token 46 | if tokens[-1] == (Token.Text, "\n"): 47 | del tokens[-1] 48 | 49 | return ((t, v) for t, v in tokens) 50 | 51 | source = filter_trailing_newline(tokensource) 52 | 53 | return super().format_unencoded(source, outfile) 54 | 55 | def _wrap_div( 56 | self, 57 | inner: TokenStream, 58 | ) -> TokenGenerator | TokenStream: 59 | styles = [] 60 | if ( 61 | self.noclasses 62 | and not self.nobackground 63 | and self.style.background_color is not None 64 | ): 65 | styles.append(f"background: {self.style.background_color}") 66 | if self.cssstyles: 67 | styles.append(self.cssstyles) 68 | style = "; ".join(styles) 69 | 70 | yield ( 71 | 0, 72 | ( 73 | "" 77 | ), 78 | ) 79 | yield from inner 80 | yield 0, "
\n" 81 | 82 | def _wrap_pre(self, inner: TokenStream) -> TokenStream: 83 | yield from inner 84 | 85 | 86 | formatter = InlineHtmlFormatter( 87 | cssclass="highlight docutils literal inline-code", 88 | noclasses=False, 89 | lineseparator="", # removes \n from end of inline snippet 90 | ) 91 | 92 | 93 | class CodeTransform(Transform): 94 | """Run over unparsed literals and try to guess language + highlight.""" 95 | 96 | default_priority = 120 97 | 98 | def apply(self, **kwargs: t.Any) -> None: 99 | """Apply CodeTransform.""" 100 | paragraph_nodes = self.document.traverse(nodes.literal) 101 | 102 | for node in paragraph_nodes: 103 | text = node.astext() 104 | 105 | newnode = None 106 | newtext = None 107 | newlexer: t.Any = None 108 | 109 | if text.startswith("$ "): 110 | from pygments.lexers.shell import BashSessionLexer 111 | 112 | from django_docutils.lib.directives.code import patch_bash_session_lexer 113 | 114 | patch_bash_session_lexer() 115 | 116 | newlexer = BashSessionLexer() 117 | elif text.startswith(("{%", "{{")): 118 | from pygments.lexers.templates import DjangoLexer 119 | 120 | newlexer = DjangoLexer() 121 | elif re.match(r"^:\w+:", text): # match :rolename: beginning 122 | from pygments.lexers.markup import RstLexer 123 | 124 | newlexer = RstLexer() # type:ignore 125 | else: 126 | from pygments.lexers import guess_lexer 127 | from pygments.lexers.mime import MIMELexer 128 | from pygments.lexers.special import TextLexer 129 | 130 | guess = guess_lexer(text) 131 | if not any(guess.__class__ != lex for lex in [MIMELexer, TextLexer]): 132 | newlexer = guess 133 | 134 | if newlexer: 135 | """Inline code can't have newlines, but we still get them: 136 | 137 | Take this reStructuredText code: 138 | 139 | .. code-block:: reStructuredText 140 | 141 | You can set options with ``$ tmux set-option`` and ``$ tmux 142 | set-window-option``. 143 | 144 | Docutils detects the separation between "tmux" and 145 | "set-window-option" in ``$ tmux set-window-options``, now as a 146 | space, but a *new line*. 147 | 148 | Let's replace the newline escape (``\n``) with a space. 149 | """ 150 | text = text.strip() # trim any whitespace around text 151 | text = text.replace("\n", " ") # switch out newlines w/ space 152 | 153 | newtext = highlight(text, newlexer, formatter) 154 | 155 | if newtext: 156 | newnode = nodes.raw("", newtext, format="html") 157 | 158 | if newnode and node.parent: 159 | node.replace_self(newnode) 160 | -------------------------------------------------------------------------------- /src/django_docutils/lib/transforms/toc.py: -------------------------------------------------------------------------------- 1 | """Django docutils table of contents helpers.""" 2 | 3 | from __future__ import annotations 4 | 5 | import sys 6 | import typing as t 7 | 8 | from docutils import nodes 9 | from docutils.transforms import parts 10 | 11 | 12 | class Contents(parts.Contents): 13 | """Helpers for rendering docutils table of contents from document tree. 14 | 15 | Changes: 16 | - Remove unused autonum 17 | - PEP8 18 | - Removed extra nodes.paragraph wrapping of list_item's. 19 | """ 20 | 21 | startnode: nodes.Node | None 22 | 23 | def build_contents( 24 | self, 25 | node: nodes.Node, 26 | level: int = 0, 27 | ) -> nodes.bullet_list | list[t.Any]: 28 | """Build nested bullet list from doctree content.""" 29 | assert isinstance(node, nodes.Element) 30 | level += 1 31 | sections: list[nodes.section] = [ 32 | sect for sect in node.children if isinstance(sect, nodes.section) 33 | ] 34 | assert self.startnode is not None 35 | entries: list[nodes.Node] = [] 36 | 37 | depth = ( 38 | self.startnode.details.get("depth", sys.maxsize) 39 | if hasattr(self.startnode, "details") 40 | else sys.maxsize 41 | ) 42 | auto = False 43 | 44 | for section in sections: 45 | title = section[0] 46 | auto = ( 47 | title.get("auto") if hasattr(title, "get") else False 48 | ) # May be set by SectNum. 49 | entrytext = self.copy_and_filter(title) 50 | reference = nodes.reference( 51 | "", 52 | "", 53 | refid=section["ids"][0], 54 | *entrytext, # noqa: B026 55 | ) 56 | ref_id = self.document.set_id(reference) 57 | item = nodes.list_item("", reference) 58 | if ( 59 | self.backlinks in {"entry", "top"} 60 | and title.next_node(nodes.reference) is None 61 | and isinstance(title, (nodes.Element, nodes.TextElement)) 62 | ): 63 | if self.backlinks == "entry": 64 | title["refid"] = ref_id 65 | elif self.backlinks == "top": 66 | title["refid"] = self.toc_id 67 | if level < depth: 68 | subsects = self.build_contents(section, level) 69 | item += subsects 70 | entries.append(item) 71 | if entries: 72 | contents = nodes.bullet_list("", *entries, classes=["menu-list"]) 73 | 74 | if auto: 75 | contents["classes"].append("auto-toc") 76 | return contents 77 | return [] 78 | -------------------------------------------------------------------------------- /src/django_docutils/lib/types.py: -------------------------------------------------------------------------------- 1 | """Typings for Django Docutils settings for django.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import NotRequired, TypedDict 6 | 7 | 8 | class DjangoDocutilsLibRSTRolesSettings(TypedDict): 9 | """Docutils role mappings.""" 10 | 11 | local: NotRequired[dict[str, str]] 12 | 13 | 14 | class DjangoDocutilsLibRSTDocutilsSettings(TypedDict): 15 | """Docutils document settings.""" 16 | 17 | raw_enabled: NotRequired[bool] 18 | strip_comments: NotRequired[bool] 19 | initial_header_level: NotRequired[int] 20 | 21 | 22 | class DjangoDocutilsLibRSTSettings(TypedDict): 23 | """Core settings object for ``DJANGO_DOCUTILS_LIB_RST``.""" 24 | 25 | metadata_processors: NotRequired[list[str]] 26 | transforms: NotRequired[list[str]] 27 | docutils: NotRequired[DjangoDocutilsLibRSTDocutilsSettings] 28 | directives: NotRequired[dict[str, str]] 29 | roles: NotRequired[DjangoDocutilsLibRSTRolesSettings] 30 | 31 | 32 | class DjangoDocutilsLibTextSettings(TypedDict): 33 | """Core settings object for ``DJANGO_DOCUTILS_TEXT_RST``.""" 34 | 35 | uncapitalized_word_filters: list[str] 36 | -------------------------------------------------------------------------------- /src/django_docutils/lib/utils.py: -------------------------------------------------------------------------------- 1 | """Docutils util functions and regexes. 2 | 3 | Some stuff is ported from sphinx: 4 | 5 | - explicit_title_re, ws_re, set_role_source_info, split_explicit_title 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import re 11 | import typing as t 12 | 13 | from docutils import nodes 14 | 15 | if t.TYPE_CHECKING: 16 | from collections.abc import Generator 17 | 18 | # \x00 means the "<" was backslash-escaped (from sphinx) 19 | explicit_title_re = re.compile(r"^(.+?)\s*(?$", re.DOTALL) 20 | 21 | ws_re: re.Pattern[str] = re.compile(r"\s+") 22 | 23 | 24 | def split_explicit_title(text: str) -> tuple[bool, str, str]: 25 | """Split role content into title and target, if given (from sphinx).""" 26 | match = explicit_title_re.match(text) 27 | if match: 28 | return True, match.group(1), match.group(2) 29 | return False, text, text 30 | 31 | 32 | def chop_after_docinfo(source: str) -> str: 33 | """Return the source of a document after DocInfo metadata. 34 | 35 | Parameters 36 | ---------- 37 | source : str 38 | Source of RST document 39 | 40 | Returns 41 | ------- 42 | str 43 | All source content after docinfo 44 | """ 45 | # find the last docinfo element 46 | index = re.findall(r":[\w_]+: [\w \-_\,]+\n", source)[-1] 47 | 48 | # find the character position of last docinfo element + len of it 49 | rest = source[source.rindex(index) + len(index) :] 50 | return rest.strip() 51 | 52 | 53 | def chop_after_title(source: str) -> str: 54 | """Return the source of a document after document title. 55 | 56 | Parameters 57 | ---------- 58 | source : str 59 | Source of RST document 60 | 61 | Returns 62 | ------- 63 | str 64 | All source content after title 65 | """ 66 | # find the last docinfo element 67 | index = re.findall(r"[=-]{3,}\n.*\n[-=]{3,}", source, re.MULTILINE)[-1] 68 | 69 | # find the character position of last docinfo element + len of it 70 | rest = source[source.rindex(index) + len(index) :] 71 | return rest.strip() 72 | 73 | 74 | def chop_after_heading_smartly(source: str) -> str: 75 | """Return the content after subtitle, or, if exists, docinfo. 76 | 77 | This is a universal chop that can be used whether a document has docinfo, 78 | a title, subtitle, or not. Traditionally, directory-style RST fixtures keep 79 | metadata inside a JSON file instead of docinfo, so 80 | :func:`chop_after_docinfo` wouldn't work. 81 | 82 | Parameters 83 | ---------- 84 | source : str 85 | Source of RST document 86 | 87 | Returns 88 | ------- 89 | str 90 | All source content after docinfo or title 91 | """ 92 | try: 93 | return chop_after_docinfo(source) 94 | except IndexError: 95 | return chop_after_title(source) 96 | 97 | 98 | def find_root_sections(document: nodes.document) -> Generator[nodes.Node, None, None]: 99 | """Yield top level section nodes. 100 | 101 | Parameters 102 | ---------- 103 | document : :class:`docutils.nodes.document` 104 | Docutils document 105 | 106 | Yields 107 | ------ 108 | :class:`docutils.nodes.Node` 109 | Upper level titles of document 110 | """ 111 | for node in document.findall(nodes.section): 112 | yield from node 113 | 114 | 115 | def append_html_to_node(node: nodes.Element, ad_code: str) -> None: 116 | """Inject HTML in this node. 117 | 118 | Parameters 119 | ---------- 120 | node : :class:`docutils.nodes.node` 121 | node of the section to find last paragraph of 122 | ad_code : str 123 | html to inject inside ad 124 | """ 125 | html = '
' 126 | html += ad_code 127 | html += "
" 128 | 129 | html_node = nodes.raw("", html, format="html") 130 | 131 | node.append(html_node) 132 | node.replace_self(node) 133 | -------------------------------------------------------------------------------- /src/django_docutils/lib/views.py: -------------------------------------------------------------------------------- 1 | """Django view machinery for rendering docutils content as HTML.""" 2 | 3 | from __future__ import annotations 4 | 5 | import pathlib 6 | import typing as t 7 | 8 | from django.utils.functional import cached_property 9 | from django.views.generic.base import ContextMixin, TemplateView 10 | 11 | from .publisher import ( 12 | publish_doctree, 13 | publish_html_from_doctree, 14 | publish_toc_from_doctree, 15 | ) 16 | from .text import smart_title 17 | 18 | if t.TYPE_CHECKING: 19 | from django.http import HttpRequest 20 | from docutils import nodes 21 | 22 | from django_docutils._internal.types import StrPath 23 | 24 | 25 | class TitleMixin(ContextMixin): 26 | """ContextMixin that capitalizes title and subtitle.""" 27 | 28 | title = None 29 | subtitle = None 30 | 31 | def get_context_data(self, **kwargs: object) -> dict[str, t.Any]: 32 | """:func:`django_docutils.lib.text.smart_title()` on title and subtitle.""" 33 | context = super().get_context_data(**kwargs) 34 | if self.title: 35 | context["title"] = smart_title(self.title) 36 | if self.subtitle: 37 | context["subtitle"] = smart_title(self.subtitle) 38 | return context 39 | 40 | 41 | class TemplateTitleView(TemplateView, TitleMixin): 42 | """Combination of Template and Title mixin.""" 43 | 44 | title = None 45 | subtitle = None 46 | 47 | def get_context_data(self, **kwargs: object) -> dict[str, t.Any]: 48 | """Return context data.""" 49 | return super().get_context_data(**kwargs) 50 | 51 | 52 | class RSTMixin: 53 | """Django Class-based view mixin for reStructuredText.""" 54 | 55 | request: HttpRequest 56 | 57 | @cached_property 58 | def raw_content(self) -> str | None: 59 | """Raw reStructuredText content.""" 60 | raise NotImplementedError 61 | 62 | @cached_property 63 | def doctree(self) -> nodes.document | None: 64 | """Return docutils doctree of RST content (pre-HTML).""" 65 | if self.raw_content is None: 66 | return None 67 | 68 | return publish_doctree(self.raw_content) 69 | 70 | @cached_property 71 | def sidebar(self, **kwargs: object) -> str | None: 72 | """Return table of contents sidebar of RST content as HTML.""" 73 | if self.doctree is None: 74 | return None 75 | 76 | return publish_toc_from_doctree(self.doctree) 77 | 78 | @cached_property 79 | def content(self) -> str | None: 80 | """Return reStructuredText content as HTML.""" 81 | if self.doctree is None: 82 | return None 83 | 84 | return publish_html_from_doctree( 85 | self.doctree, 86 | **getattr(self, "rst_settings", {}), 87 | ) 88 | 89 | def get_base_template(self) -> str: 90 | """TODO: move this out of RSTMixin, it is AMP related, not RST.""" 91 | if self.request.GET.get("is_amp", False): 92 | return "django_docutils/base-amp.html" 93 | return "base.html" 94 | 95 | 96 | class RSTRawView(TemplateTitleView): 97 | """Send pure reStructuredText to template. 98 | 99 | Requires template tags to process it. 100 | 101 | .. code-block:: django 102 | 103 | {% load django_docutils %} 104 | {% block content %} 105 |
106 | {% restructuredtext content show_title=False %} 107 |
108 | {% endblock content %} 109 | 110 | {% block sidebar %} 111 | {% restructuredtext content toc_only=True %} 112 | {% endblock sidebar %} 113 | 114 | """ 115 | 116 | template_name = "rst/raw.html" 117 | file_path: StrPath | None = None 118 | title = None 119 | 120 | def get_context_data(self, **kwargs: object) -> dict[str, t.Any]: 121 | """Merge content to context data.""" 122 | context = super().get_context_data(**kwargs) 123 | 124 | if self.file_path is not None: 125 | with pathlib.Path(self.file_path).open(encoding="utf-8") as content: 126 | context["content"] = content.read() 127 | 128 | return context 129 | 130 | 131 | class RSTView(RSTRawView, RSTMixin): 132 | """RestructuredText Django View.""" 133 | 134 | template_name = "rst/base.html" 135 | file_path: StrPath | None = None 136 | title = None 137 | 138 | @cached_property 139 | def raw_content(self) -> str | None: 140 | """Raw reStructuredText data.""" 141 | if self.file_path is None: 142 | return None 143 | 144 | with pathlib.Path(self.file_path).open(encoding="utf-8") as raw_content: 145 | return raw_content.read() 146 | 147 | def get_context_data(self, **kwargs: object) -> dict[str, t.Any]: 148 | """Merge content and sidebar to context data.""" 149 | context = super().get_context_data(**kwargs) 150 | context["content"] = self.content 151 | context["sidebar"] = self.sidebar 152 | 153 | return context 154 | -------------------------------------------------------------------------------- /src/django_docutils/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-docutils/09ec125351a6dfcc05fafb4af6a297fac4ddb14a/src/django_docutils/py.typed -------------------------------------------------------------------------------- /src/django_docutils/template.py: -------------------------------------------------------------------------------- 1 | """Django template engine for Docutils.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django.conf import settings 8 | from django.template.backends.base import BaseEngine 9 | from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy 10 | from django.template.engine import Engine 11 | from django.template.exceptions import TemplateDoesNotExist 12 | from django.utils.safestring import mark_safe 13 | from docutils import core 14 | 15 | from django_docutils.lib.directives.code import register_pygments_directive 16 | 17 | if t.TYPE_CHECKING: 18 | from django.http.request import HttpRequest 19 | from django.template.backends.base import _EngineTemplate 20 | from django.template.base import Context 21 | from django.utils.safestring import SafeString 22 | 23 | 24 | class DocutilsTemplates(BaseEngine): 25 | """Docutils engine for Django.""" 26 | 27 | app_dirname: str = "templates" 28 | 29 | def __init__(self, params: dict[str, t.Any]) -> None: 30 | params = params.copy() 31 | self.options = params.pop("OPTIONS").copy() 32 | self.options.setdefault("debug", settings.DEBUG) 33 | super().__init__(params) 34 | self.engine = Engine(self.dirs, self.app_dirs, **self.options) 35 | 36 | def from_string(self, template_code: str) -> DocutilsTemplate: 37 | """Return DocutilsTemplate from string.""" 38 | return DocutilsTemplate(template_code, self.options) 39 | 40 | def get_template(self, template_name: str) -> _EngineTemplate: 41 | """Return template from template_name.""" 42 | for template_file in self.iter_template_filenames(template_name): 43 | try: 44 | with open(template_file, encoding="utf-8") as fp: 45 | template_code = fp.read() 46 | except OSError: 47 | continue 48 | 49 | return DocutilsTemplate(template_code, self.options) 50 | raise TemplateDoesNotExist(template_name) 51 | 52 | 53 | class DocutilsTemplate: 54 | """Docutils template object for Django. Used by Docutils template engine.""" 55 | 56 | def __init__(self, source: str, options: dict[str, t.Any]) -> None: 57 | self.source = source 58 | self.options = options 59 | 60 | def render( 61 | self, 62 | context: Context | dict[str, t.Any] | None = None, 63 | request: HttpRequest | None = None, 64 | ) -> SafeString: 65 | """Render DocutilsTemplate to string.""" 66 | context = self.options 67 | if request is not None: 68 | context["request"] = request 69 | context["csrf_input"] = csrf_input_lazy(request) 70 | context["csrf_token"] = csrf_token_lazy(request) 71 | context = {"source": self.source, "writer_name": "html"} 72 | 73 | parts = core.publish_parts(**context)["html_body"] 74 | assert isinstance(parts, str) 75 | 76 | return mark_safe(parts) 77 | 78 | 79 | register_pygments_directive() 80 | -------------------------------------------------------------------------------- /src/django_docutils/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for Django templatetags for docutils.""" 2 | -------------------------------------------------------------------------------- /src/django_docutils/templatetags/django_docutils.py: -------------------------------------------------------------------------------- 1 | """Django template tag and filter for docutils (rendering reStructuredText as HTML).""" 2 | 3 | from __future__ import annotations 4 | 5 | from django import template 6 | from django.template.base import FilterExpression, Node, Parser, Token, kwarg_re 7 | from django.template.context import Context 8 | from django.template.exceptions import TemplateSyntaxError 9 | from django.utils.encoding import force_str 10 | from django.utils.safestring import SafeString, mark_safe 11 | 12 | from django_docutils.lib.publisher import publish_html_from_source 13 | 14 | register = template.Library() 15 | 16 | 17 | class ReStructuredTextNode(Node): 18 | """Implement the actions of the rst tag.""" 19 | 20 | def __init__( 21 | self, 22 | content: FilterExpression | str, 23 | args: list[FilterExpression] | None = None, 24 | kwargs: dict[str, FilterExpression] | None = None, 25 | asvar: str | None = None, 26 | ) -> None: 27 | self.content = content 28 | self.args = args if args is not None else [] 29 | self.kwargs = kwargs if kwargs is not None else {} 30 | self.asvar = asvar 31 | 32 | def render(self, context: Context | None = None) -> str: 33 | """Render Node as string.""" 34 | if context is None: 35 | context = Context() 36 | 37 | args = [arg.resolve(context) for arg in self.args] 38 | kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()} 39 | 40 | if isinstance(self.content, FilterExpression): 41 | content = self.content.resolve(context) 42 | else: 43 | content = self.content 44 | 45 | html = publish_html_from_source(content, *args, **kwargs) 46 | if html is None: 47 | return "" 48 | return html 49 | 50 | 51 | class MalformedArgumentsToRSTTag(TemplateSyntaxError): 52 | """Invalid arguments to rst django template tag.""" 53 | 54 | def __init__(self, *args: object, **kwargs: object) -> None: 55 | return super().__init__("Malformed arguments to url tag", *args, **kwargs) 56 | 57 | 58 | @register.tag 59 | def rst(parser: Parser, token: Token) -> ReStructuredTextNode: 60 | """Django template tag to render reStructuredText as HTML. 61 | 62 | Supports arguments, see below. 63 | 64 | Examples 65 | -------- 66 | .. code-block:: django 67 | 68 | {% rst content %} 69 | 70 | .. code-block:: django 71 | 72 | {% rst content toc_only=True %} 73 | 74 | .. code-block:: django 75 | 76 | {% rst content show_title=False %} 77 | 78 | .. code-block:: django 79 | 80 | {% rst %} 81 | **Hello world** 82 | {% endrst %} 83 | 84 | Render table of contents: 85 | 86 | .. code-block:: django 87 | 88 | {% rst toc_only=True %} 89 | Welcome to my site! 90 | =================== 91 | 92 | My header 93 | --------- 94 | 95 | Some text 96 | 97 | Additional information 98 | ---------------------- 99 | 100 | Thank you 101 | {% endrst %} 102 | """ 103 | bits = token.split_contents() 104 | args = [] 105 | kwargs = {} 106 | asvar = None 107 | 108 | content: FilterExpression | SafeString | None = None 109 | 110 | if len(bits) >= 2 and bits[1] == "content": 111 | content = parser.compile_filter(bits[1]) 112 | bits = bits[2:] # Chop off "rst content" 113 | else: 114 | nodelist = parser.parse(("endrst",)) 115 | parser.delete_first_token() 116 | content = nodelist.render(Context()) 117 | bits = bits[1:] # Chop off "rst" 118 | if len(bits) >= 2 and bits[-2] == "as": 119 | asvar = bits[-1] 120 | bits = bits[:-2] 121 | 122 | if len(bits): 123 | for bit in bits: 124 | match = kwarg_re.match(bit) 125 | if not match: 126 | raise MalformedArgumentsToRSTTag 127 | 128 | name, value = match.groups() 129 | if name: 130 | kwargs[name] = parser.compile_filter(value) 131 | else: 132 | args.append(parser.compile_filter(value)) 133 | 134 | # TODO: Raise if no end tag found 135 | # if len(bits) < 2: 136 | # raise TemplateSyntaxError( 137 | # "'%s' takes at least one argument, a content param." % bits[0] 138 | # ) 139 | 140 | return ReStructuredTextNode(content, args, kwargs, asvar) 141 | 142 | 143 | @register.filter(name="rst", is_safe=True) 144 | def rst_filter(value: str) -> str: 145 | """Django template filter to render reStructuredText (rst) as HTML.""" 146 | import warnings 147 | 148 | warnings.warn( 149 | "The rst filter has been deprecated", 150 | category=DeprecationWarning, 151 | stacklevel=3, 152 | ) 153 | 154 | return mark_safe(force_str(ReStructuredTextNode(value).render())) 155 | -------------------------------------------------------------------------------- /src/django_docutils/views.py: -------------------------------------------------------------------------------- 1 | """Django-docutils class-based view for django (and its' parts).""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django.core.exceptions import ImproperlyConfigured 8 | from django.template.loader import select_template 9 | from django.template.response import TemplateResponse 10 | from django.views.generic.base import TemplateView 11 | 12 | if t.TYPE_CHECKING: 13 | from django.http.request import HttpRequest 14 | from django.http.response import HttpResponse 15 | 16 | 17 | class DocutilsResponse(TemplateResponse): 18 | """Docutils TemplateResponse.""" 19 | 20 | template_name = "base.html" 21 | 22 | def __init__( 23 | self, 24 | request: HttpRequest, 25 | template: list[str], 26 | rst: list[str], 27 | context: dict[str, t.Any] | None = None, 28 | content_type: str | None = None, 29 | status: int | None = None, 30 | charset: str | None = None, 31 | using: str | None = None, 32 | ) -> None: 33 | self.rst_name = rst 34 | super().__init__( 35 | request, 36 | template, 37 | context, 38 | content_type, 39 | status, 40 | charset, 41 | using, 42 | ) 43 | 44 | @property 45 | def rendered_content(self) -> str: 46 | """Return freshly rendered content via docutils engine.""" 47 | context: dict[str, t.Any] = self.resolve_context(self.context_data) or {} 48 | 49 | # we should be able to use the engine to .Render this 50 | from django.utils.safestring import mark_safe 51 | 52 | context["content"] = mark_safe( 53 | select_template(self.rst_name, using="docutils").render(), 54 | ) 55 | 56 | template = self.resolve_template(self.template_name) 57 | return template.render(context) # type:ignore 58 | 59 | 60 | class DocutilsViewRstNameImproperlyConfigured(ImproperlyConfigured): 61 | """DocutilsView could not find rst_name.""" 62 | 63 | def __init__(self, *args: object, **kwargs: object) -> None: 64 | return super().__init__( 65 | "DocutilsView requires either a definition of 'rst_name' or an " 66 | "implementation of 'get_rst_names()'", 67 | *args, 68 | **kwargs, 69 | ) 70 | 71 | 72 | class DocutilsView(TemplateView): 73 | """Django-docutils view, renders reStructuredText to HTML via rst_name.""" 74 | 75 | response_class = DocutilsResponse 76 | rst_name: str | None = None 77 | 78 | def render_to_response( 79 | self, 80 | context: dict[str, t.Any] | None = None, 81 | content_type: str | None = None, 82 | status: int | None = None, 83 | charset: str | None = None, 84 | using: str | None = None, 85 | **response_kwargs: object, 86 | ) -> HttpResponse: 87 | """Override to pay in rst content.""" 88 | return self.response_class( 89 | request=self.request, 90 | template=self.get_template_names(), 91 | rst=self.get_rst_names(), 92 | context=context, 93 | content_type=content_type, 94 | status=status, 95 | using=using or self.template_engine, 96 | ) 97 | 98 | def get_rst_names(self) -> list[str]: 99 | """Follows after get_template_names, but for scanning for rst content.""" 100 | if self.rst_name is None: 101 | raise DocutilsViewRstNameImproperlyConfigured 102 | return [self.rst_name] 103 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Django-docutils tests.""" 2 | -------------------------------------------------------------------------------- /tests/constants.py: -------------------------------------------------------------------------------- 1 | """Constants for Django Docutils test suite.""" 2 | 3 | from __future__ import annotations 4 | 5 | DEFAULT_RST = r""" 6 | hey 7 | --- 8 | 9 | hi 10 | ## 11 | 12 | A. hows 13 | B. it 14 | 15 | C. going 16 | D. today 17 | 18 | **hi** 19 | *hi* 20 | """ 21 | 22 | DEFAULT_RST_WITH_SECTIONS = """ 23 | hey 24 | --- 25 | 26 | hi 27 | ## 28 | 29 | My first section 30 | ---------------- 31 | 32 | Some text 33 | 34 | My second section 35 | ----------------- 36 | 37 | Additional text 38 | 39 | A. hows 40 | B. it 41 | 42 | C. going 43 | D. today 44 | 45 | **hi** 46 | *hi* 47 | """ 48 | 49 | 50 | DEFAULT_EXPECTED_CONTENT = r""" 51 |
    52 |
  1. hows

  2. 53 |
  3. it

  4. 54 |
  5. going

  6. 55 |
  7. today

  8. 56 |
57 |

hi 58 | hi

59 | """.strip() 60 | DEFAULT_EXPECTED = rf""" 61 |
62 |

hey

63 |

hi

64 | {DEFAULT_EXPECTED_CONTENT} 65 |
66 | 67 | """ 68 | -------------------------------------------------------------------------------- /tests/rst_content/home.rst: -------------------------------------------------------------------------------- 1 | hey 2 | --- 3 | 4 | hi 5 | ## 6 | 7 | A. hows 8 | B. it 9 | 10 | C. going 11 | D. today 12 | 13 | **hi** 14 | *hi* 15 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """django-docutils test settings module for django.""" 2 | 3 | from __future__ import annotations 4 | 5 | import pathlib 6 | import typing as t 7 | 8 | import django 9 | 10 | if t.TYPE_CHECKING: 11 | from django_docutils.lib.types import DjangoDocutilsLibRSTSettings 12 | 13 | if django.VERSION <= (4, 2): 14 | USE_L10N = True 15 | 16 | if django.VERSION <= (4, 1): 17 | DEFAULT_FILE_STORAGE = "inmemorystorage.InMemoryStorage" 18 | else: 19 | STORAGES = { 20 | "default": { 21 | "BACKEND": "inmemorystorage.InMemoryStorage", 22 | }, 23 | } 24 | 25 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 26 | SECRET_KEY = "not very secret in tests" 27 | USE_TZ = True 28 | TEMPLATES = [ 29 | { 30 | "BACKEND": "django.template.backends.django.DjangoTemplates", 31 | "APP_DIRS": True, 32 | "DIRS": [pathlib.Path(__file__).parent / "templates"], 33 | }, 34 | { 35 | "NAME": "docutils", 36 | "BACKEND": "django_docutils.template.DocutilsTemplates", 37 | "APP_DIRS": True, 38 | "DIRS": [pathlib.Path(__file__).parent / "rst_content"], 39 | }, 40 | ] 41 | DJANGO_DOCUTILS_ANONYMOUS_USER_NAME = "AnonymousCoward" 42 | DJANGO_DOCUTILS_LIB_RST: DjangoDocutilsLibRSTSettings = { 43 | "metadata_processors": [ 44 | "django_docutils.lib.metadata.processors.process_datetime", 45 | "django_docutils.lib.metadata.processors.process_anonymous_user", 46 | ], 47 | "transforms": [ #: docutils.transforms.Transform class (import string) 48 | "django_docutils.lib.transforms.code.CodeTransform", 49 | ], 50 | "docutils": { #: Used in restructuredtext templatetags 51 | "raw_enabled": True, 52 | "strip_comments": True, 53 | "initial_header_level": 2, 54 | }, 55 | "directives": { #: directive-name: Directive class (import string) 56 | "code-block": "django_docutils.lib.directives.code.CodeBlock", 57 | }, 58 | "roles": { 59 | "local": { 60 | "gh": "django_docutils.lib.roles.github.github_role", 61 | "pypi": "django_docutils.lib.roles.pypi.pypi_role", 62 | "kbd": "django_docutils.lib.roles.kbd.kbd_role", 63 | "file": "django_docutils.lib.roles.file.file_role", 64 | "exe": "django_docutils.lib.roles.file.exe_role", 65 | "manifest": "django_docutils.lib.roles.file.manifest_role", 66 | "rtd": "django_docutils.lib.roles.readthedocs.readthedocs_role", 67 | "url": "django_docutils.lib.roles.url.url_role", 68 | "leanpub": "django_docutils.lib.roles.leanpub.leanpub_role", 69 | "twitter": "django_docutils.lib.roles.twitter.twitter_role", 70 | "email": "django_docutils.lib.roles.email.email_role", 71 | "hn": "django_docutils.lib.roles.hackernews.hackernews_role", 72 | "wikipedia": "django_docutils.lib.roles.wikipedia.wikipedia_role", 73 | }, 74 | }, 75 | } 76 | DJANGO_DOCUTILS_LIB_TEXT: dict[str, list[str]] = { # Optional 77 | "uncapitalized_word_filters": ["project.my_module.my_capitalization_fn"], 78 | } 79 | INSTALLED_APPS = ("django_docutils",) 80 | -------------------------------------------------------------------------------- /tests/templates/base.html: -------------------------------------------------------------------------------- 1 | {{content}} 2 | -------------------------------------------------------------------------------- /tests/test_docutils_roles.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: E501 2 | """Tests for docutils roles.""" 3 | 4 | from __future__ import annotations 5 | 6 | import typing as t 7 | 8 | import pytest 9 | from django.template import Context, Template 10 | 11 | from django_docutils.lib.roles.registry import ( 12 | register_django_docutils_roles, 13 | register_role_mapping, 14 | ) 15 | 16 | if t.TYPE_CHECKING: 17 | from collections.abc import Sequence 18 | 19 | from docutils import nodes 20 | from docutils.parsers.rst.states import Inliner 21 | 22 | MAIN_TPL = """ 23 |
24 | {content} 25 |
26 | 27 | """ 28 | 29 | 30 | def render_rst_block(contents: str, context: t.Any = None) -> str: 31 | """Render django template tag {% rst %}{{contents}}{% endrst %}, minus main wrap.""" 32 | if context is None: 33 | context = {} 34 | template = Template( 35 | """{% load django_docutils %} 36 | {% rst %} 37 | {{DEFAULT_RST}} 38 | {% endrst %} 39 | """.replace("{{DEFAULT_RST}}", contents), 40 | ) 41 | 42 | return template.render(Context(context)) 43 | 44 | 45 | class RoleContentFixture(t.NamedTuple): 46 | """Test docutils role -> django HTML template.""" 47 | 48 | # pytest 49 | test_id: str 50 | 51 | # Assertions 52 | rst_content: str 53 | expected_html: str 54 | 55 | 56 | def test_register_django_docutils_roles(monkeypatch: pytest.MonkeyPatch) -> None: 57 | """Assertions for register_django_docutils_roles().""" 58 | from django_docutils.lib.roles import registry as roles_registry_pkg 59 | 60 | assert roles_registry_pkg.DJANGO_DOCUTILS_LIB_RST, ( # type:ignore[attr-defined] 61 | "Sanity-check, something truthy should be set." 62 | ) 63 | register_django_docutils_roles() 64 | 65 | monkeypatch.setattr(roles_registry_pkg, "DJANGO_DOCUTILS_LIB_RST", {}) 66 | register_django_docutils_roles() 67 | 68 | monkeypatch.setattr(roles_registry_pkg, "DJANGO_DOCUTILS_LIB_RST", {"other": None}) 69 | register_django_docutils_roles() 70 | 71 | monkeypatch.setattr(roles_registry_pkg, "DJANGO_DOCUTILS_LIB_RST", {"roles": {}}) 72 | register_django_docutils_roles() 73 | 74 | 75 | class SphinxLikeRole: 76 | """A base class copied from SphinxRole for testing class-based roles. 77 | 78 | This class provides helper methods for Sphinx-like roles. 79 | 80 | .. note:: The subclasses of this class might not work with docutils. 81 | This class is strongly coupled with Sphinx. 82 | """ 83 | 84 | name: str #: The role name actually used in the document. 85 | rawtext: str #: A string containing the entire interpreted text input. 86 | text: str #: The interpreted text content. 87 | lineno: int #: The line number where the interpreted text begins. 88 | inliner: Inliner #: The ``docutils.parsers.rst.states.Inliner`` object. 89 | #: A dictionary of directive options for customisation 90 | #: (from the "role" directive). 91 | options: dict[str, t.Any] 92 | #: A list of strings, the directive content for customisation 93 | #: (from the "role" directive). 94 | content: Sequence[str] 95 | 96 | def __call__( 97 | self, 98 | name: str, 99 | rawtext: str, 100 | text: str, 101 | lineno: int, 102 | inliner: Inliner, 103 | options: dict[str, t.Any] | None = None, 104 | content: Sequence[str] = (), 105 | ) -> tuple[list[nodes.Node], list[t.Any]]: 106 | """Return example class-based role.""" 107 | self.rawtext = rawtext 108 | self.text = text 109 | self.lineno = lineno 110 | self.inliner = inliner 111 | self.options = options if options is not None else {} 112 | self.content = content 113 | 114 | # guess role type 115 | if name: 116 | self.name = name.lower() 117 | return self.run() 118 | 119 | def run(self) -> tuple[list[nodes.Node], list[t.Any]]: 120 | """Run docutils role.""" 121 | raise NotImplementedError 122 | 123 | 124 | MySphinxLikeRole = SphinxLikeRole() 125 | 126 | 127 | def test_register_role_mapping() -> None: 128 | """Assertions for register_role_mapping().""" 129 | register_role_mapping({}) 130 | 131 | register_role_mapping({"gh": "django_docutils.lib.roles.github.github_role"}) 132 | 133 | register_role_mapping( 134 | { 135 | "gh": ( 136 | "django_docutils.lib.roles.github.github_role", 137 | { 138 | "lowercase": True, 139 | "innernodeclass": "docutils.nodes.inline", 140 | "warn_dangling": True, 141 | }, 142 | ), 143 | }, 144 | ) 145 | 146 | register_role_mapping( 147 | { 148 | "ex": ( 149 | "tests.test_docutils_roles.MySphinxLikeRole", 150 | { 151 | "lowercase": True, 152 | "innernodeclass": "docutils.nodes.inline", 153 | "warn_dangling": True, 154 | }, 155 | ), 156 | }, 157 | ) 158 | 159 | register_role_mapping( 160 | { 161 | "ex": "tests.test_docutils_roles.MySphinxLikeRole", 162 | }, 163 | ) 164 | 165 | 166 | GH_ROLE_TESTS: list[RoleContentFixture] = [ 167 | RoleContentFixture( 168 | test_id="gh-role-org", 169 | rst_content=":gh:`org`\n", 170 | expected_html='

org

', 171 | ), 172 | RoleContentFixture( 173 | test_id="gh-role-org", 174 | rst_content=":gh:`org/repo`\n", 175 | expected_html='

org/repo

', 176 | ), 177 | RoleContentFixture( 178 | test_id="gh-role-org", 179 | rst_content=":gh:`My repo `\n", 180 | expected_html='

My repo

', 181 | ), 182 | RoleContentFixture( 183 | test_id="gh-role-org", 184 | rst_content=":gh:`org/repo#125`\n", 185 | expected_html='

org/repo#125

', 186 | ), 187 | RoleContentFixture( 188 | test_id="gh-role-org", 189 | rst_content="My repo :gh:`(#125) `\n", 190 | expected_html='

My repo (#125)

', 191 | ), 192 | ] 193 | 194 | 195 | @pytest.mark.parametrize( 196 | RoleContentFixture._fields, 197 | GH_ROLE_TESTS, 198 | ids=[f.test_id for f in GH_ROLE_TESTS], 199 | ) 200 | def test_templatetag_gh_role( 201 | settings: t.Any, 202 | test_id: str, 203 | rst_content: str, 204 | expected_html: str, 205 | ) -> None: 206 | """Asserts gh docutils role.""" 207 | assert render_rst_block(rst_content) == MAIN_TPL.format(content=expected_html) 208 | -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | """Tests for DocutilsView template view.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django_docutils.views import DocutilsView 8 | 9 | from .constants import DEFAULT_RST 10 | 11 | if t.TYPE_CHECKING: 12 | import pathlib 13 | 14 | from django.test import RequestFactory 15 | 16 | 17 | def test_view(settings: t.Any, tmp_path: pathlib.Path, rf: RequestFactory) -> None: 18 | """Assert DocutilsView renders HTML from reStructuredText.""" 19 | request = rf.get("/") 20 | template_dir = tmp_path / "templates" 21 | if not template_dir.exists(): 22 | template_dir.mkdir() 23 | settings.TEMPLATES[0].setdefault("DIRS", [str(template_dir)]) 24 | settings.TEMPLATES.append( 25 | { 26 | "NAME": "docutils", 27 | "BACKEND": "django_docutils.template.DocutilsTemplates", 28 | "DIRS": [str(template_dir)], 29 | "APP_DIRS": True, 30 | }, 31 | ) 32 | base_html = template_dir / "base.html" 33 | base_html.write_text("{{content}}", encoding="utf-8") 34 | 35 | home_rst = template_dir / "home.rst" 36 | home_rst.write_text( 37 | DEFAULT_RST, 38 | encoding="utf-8", 39 | ) 40 | 41 | class HomeView(DocutilsView): 42 | template_name = "base.html" 43 | rst_name = "home.rst" 44 | 45 | home_view = HomeView() 46 | home_view.setup(request) 47 | rendered_response = home_view.render_to_response() 48 | rendered_response.render() # type:ignore 49 | assert ( 50 | rendered_response.content.decode("utf-8") 51 | == r"""
52 |

hey

53 |

hi

54 |
    55 |
  1. hows
  2. 56 |
  3. it
  4. 57 |
  5. going
  6. 58 |
  7. today
  8. 59 |
60 |

hi 61 | hi

62 |
63 | 64 | """ 65 | ) 66 | -------------------------------------------------------------------------------- /tests/test_templatetag.py: -------------------------------------------------------------------------------- 1 | """Tests for rst template filter and tags.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | import pytest 8 | from django.template import Context, Template 9 | 10 | from .constants import ( 11 | DEFAULT_EXPECTED, 12 | DEFAULT_EXPECTED_CONTENT, 13 | DEFAULT_RST, 14 | DEFAULT_RST_WITH_SECTIONS, 15 | ) 16 | 17 | 18 | def test_filter(settings: t.Any) -> None: 19 | """Assert rst filter via block renders HTML.""" 20 | template = Template( 21 | r"""{% load django_docutils %} 22 | {% filter rst %} 23 | {{DEFAULT_RST}} 24 | {% endfilter %} 25 | """.replace("{{DEFAULT_RST}}", DEFAULT_RST), 26 | ) 27 | with pytest.warns(DeprecationWarning) as record: 28 | assert template.render(Context()) == DEFAULT_EXPECTED 29 | message = record[0].message 30 | assert isinstance(message, Warning) 31 | assert message.args[0] == "The rst filter has been deprecated" 32 | 33 | 34 | def test_templatetag(settings: t.Any) -> None: 35 | """Asserts rst block via variable renders HTML content.""" 36 | template = Template( 37 | """{% load django_docutils %} 38 | {% rst content %} 39 | """, 40 | ) 41 | 42 | assert template.render(Context({"content": DEFAULT_RST})) == DEFAULT_EXPECTED 43 | 44 | 45 | def test_templatetag_show_title(settings: t.Any) -> None: 46 | """Asserts rst template via variable that preserves title.""" 47 | template = Template( 48 | """{% load django_docutils %} 49 | {% rst content show_title=False %} 50 | """.strip(), 51 | ) 52 | 53 | assert ( 54 | template.render(Context({"content": DEFAULT_RST})) 55 | == "\n" + DEFAULT_EXPECTED_CONTENT + "\n" 56 | ) 57 | 58 | 59 | def test_templatetag_toc_only(settings: t.Any) -> None: 60 | """Asserts rst template via variable w/ toc_only=True renders table of contents.""" 61 | template = Template( 62 | """{% load django_docutils %} 63 | {% rst content toc_only=True %} 64 | """.strip(), 65 | ) 66 | 67 | assert ( 68 | template.render(Context({"content": DEFAULT_RST_WITH_SECTIONS})) 69 | == """ 70 |
71 | 72 | 86 |
87 | """ # noqa: E501 88 | ) 89 | 90 | 91 | def test_templatetag_toc_only_block(settings: t.Any) -> None: 92 | """Asserts rst template via block w/ toc_only=True renders table of contents.""" 93 | template = Template( 94 | """{% load django_docutils %} 95 | {% rst toc_only=True %} 96 | {{DEFAULT_RST_WITH_SECTIONS}} 97 | {% endrst %} 98 | """.replace("{{DEFAULT_RST_WITH_SECTIONS}}", DEFAULT_RST_WITH_SECTIONS).strip(), 99 | ) 100 | 101 | assert ( 102 | template.render(Context()) 103 | == """ 104 |
105 | 106 | 120 |
121 | """ # noqa: E501 122 | ) 123 | 124 | 125 | def test_templatetag_block(settings: t.Any) -> None: 126 | """Asserts rst template block render HTML content.""" 127 | template = Template( 128 | """{% load django_docutils %} 129 | {% rst %} 130 | {{DEFAULT_RST}} 131 | {% endrst %} 132 | """.replace("{{DEFAULT_RST}}", DEFAULT_RST), 133 | ) 134 | 135 | assert template.render(Context()) == DEFAULT_EXPECTED 136 | 137 | 138 | def test_templatetag_roles(settings: t.Any) -> None: 139 | """Template tag utilizes custom roles, detects new ones on setting update.""" 140 | settings.DJANGO_DOCUTILS_LIB_RST = { 141 | "roles": {"local": {}}, 142 | } 143 | 144 | template = Template( 145 | """{% load django_docutils %} 146 | {% rst %} 147 | :custom_role:`myorg/myrepo` 148 | {% endrst %} 149 | """, 150 | ) 151 | 152 | assert template.render(Context()) == ( 153 | """ 154 |
155 |

:custom_role:`myorg/myrepo`

156 | 160 |
161 | 162 | """ # noqa: E501 163 | ) 164 | 165 | settings.DJANGO_DOCUTILS_LIB_RST = { 166 | "roles": { 167 | "local": { 168 | "custom_role": "django_docutils.lib.roles.github.github_role", 169 | }, 170 | }, 171 | } 172 | 173 | assert template.render(Context()) == ( 174 | """ 175 |
176 |

myorg/myrepo

177 |
178 | 179 | """ # noqa: E501 180 | ) 181 | 182 | 183 | def test_templatetag_directive(settings: t.Any) -> None: 184 | """Template tag utilizes custom directives, detects new ones on setting update.""" 185 | settings.DJANGO_DOCUTILS_LIB_RST = { 186 | "directives": {}, 187 | } 188 | 189 | template = Template( 190 | """{% load django_docutils %} 191 | {% rst %} 192 | .. moo-block:: python 193 | 194 | import this 195 | {% endrst %} 196 | """, 197 | ) 198 | 199 | assert template.render(Context()) == ( 200 | """ 201 |
202 | 209 |
210 | 211 | """ # noqa: E501 212 | ) 213 | 214 | settings.DJANGO_DOCUTILS_LIB_RST = { 215 | "directives": {"moo-block": "django_docutils.lib.directives.code.CodeBlock"}, 216 | } 217 | 218 | assert template.render(Context()) == ( 219 | """ 220 |
221 |
import this
222 | 
223 |
224 | 225 | """ # noqa: E501 226 | ) 227 | --------------------------------------------------------------------------------