├── .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-slugify-processor.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.md ├── conf.py ├── developing.md ├── history.md ├── index.md ├── make.bat ├── manifest.json └── redirects.txt ├── pyproject.toml ├── src └── django_slugify_processor │ ├── __about__.py │ ├── __init__.py │ ├── py.typed │ ├── templatetags │ ├── __init__.py │ └── slugify_processor.py │ └── text.py ├── test_app ├── __init__.py ├── coding.py └── models.py ├── tests ├── __init__.py ├── settings.py ├── test_filters.py ├── test_test_project.py └── test_text.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 | - name: Filter changed file paths to outputs 17 | uses: dorny/paths-filter@v3.0.2 18 | id: changes 19 | with: 20 | filters: | 21 | root_docs: 22 | - CHANGES 23 | - README.* 24 | docs: 25 | - 'docs/**' 26 | - 'examples/**' 27 | python_files: 28 | - 'src/django_slugify_processor/**' 29 | - pyproject.toml 30 | - uv.lock 31 | 32 | - name: Should publish 33 | if: steps.changes.outputs.docs == 'true' || steps.changes.outputs.root_docs == 'true' || steps.changes.outputs.python_files == 'true' 34 | run: echo "PUBLISH=$(echo true)" >> $GITHUB_ENV 35 | 36 | - name: Install uv 37 | if: env.PUBLISH == 'true' 38 | uses: astral-sh/setup-uv@v5 39 | with: 40 | enable-cache: true 41 | 42 | - name: Set up Python ${{ matrix.python-version }} 43 | if: env.PUBLISH == 'true' 44 | run: uv python install ${{ matrix.python-version }} 45 | 46 | - name: Install dependencies [w/ docs] 47 | if: env.PUBLISH == 'true' 48 | run: uv sync --all-extras --dev 49 | 50 | - name: Print python versions 51 | if: env.PUBLISH == 'true' 52 | run: | 53 | python -V 54 | uv run python -V 55 | 56 | - name: Build documentation 57 | if: env.PUBLISH == 'true' 58 | run: | 59 | pushd docs; make SPHINXBUILD='uv run sphinx-build' html; popd 60 | 61 | - name: Push documentation to S3 62 | if: env.PUBLISH == 'true' 63 | uses: jakejarvis/s3-sync-action@v0.5.1 64 | with: 65 | args: --acl public-read --follow-symlinks --delete 66 | env: 67 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 68 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 69 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 70 | AWS_REGION: 'us-west-1' # optional: defaults to us-east-1 71 | SOURCE_DIR: 'docs/_build/html' # optional: defaults to entire repository 72 | 73 | - name: Purge cache on Cloudflare 74 | if: env.PUBLISH == 'true' 75 | uses: jakejarvis/cloudflare-purge-action@v0.3.0 76 | env: 77 | CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }} 78 | CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }} 79 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 8 | 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.10', '3.13'] 13 | django-version: ['4.2', '5.1'] 14 | include: 15 | - python-version: '3.9' 16 | django-version: '4.2' 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install uv 21 | uses: astral-sh/setup-uv@v5 22 | with: 23 | enable-cache: true 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | run: uv python install ${{ matrix.python-version }} 27 | 28 | - name: Install dependencies 29 | run: uv sync --all-extras --dev 30 | 31 | - name: Install django @ ${{ matrix.django-version }} 32 | run: uv pip install DJANGO~=${{ matrix.django-version }} 33 | 34 | - name: Install django-extensions @ ${{ matrix.django-extensions }}, if exists 35 | if: ${{ matrix.django-extensions != '' }} 36 | run: uv pip install 'django-extensions${{ matrix.django-extensions }}' 37 | 38 | - name: Print python versions 39 | run: | 40 | python -V 41 | uv run python -V 42 | 43 | - name: Lint with ruff check 44 | run: uv run ruff check . 45 | 46 | - name: Format with ruff format 47 | run: uv run ruff format . --check 48 | 49 | - name: Lint with mypy 50 | run: uv run mypy . 51 | 52 | - name: Test with pytest 53 | run: uv run py.test --cov=./ --cov-report=xml 54 | 55 | - uses: codecov/codecov-action@v5 56 | with: 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | 59 | release: 60 | runs-on: ubuntu-latest 61 | needs: build 62 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 63 | 64 | strategy: 65 | matrix: 66 | python-version: ['3.13'] 67 | 68 | steps: 69 | - uses: actions/checkout@v4 70 | 71 | - name: Install uv 72 | uses: astral-sh/setup-uv@v5 73 | with: 74 | enable-cache: true 75 | 76 | - name: Set up Python ${{ matrix.python-version }} 77 | run: uv python install ${{ matrix.python-version }} 78 | 79 | - name: Build package 80 | run: uv build 81 | 82 | - name: Publish package 83 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 84 | uses: pypa/gh-action-pypi-publish@release/v1 85 | with: 86 | user: __token__ 87 | password: ${{ secrets.PYPI_API_TOKEN }} 88 | skip_existing: true 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # sphinx 104 | doc/_build 105 | 106 | # pytest 107 | .pytest_cache/ 108 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13.0 2 | -------------------------------------------------------------------------------- /.tmuxp.yaml: -------------------------------------------------------------------------------- 1 | session_name: django-slugify-processor 2 | start_directory: ./ # load session relative to config location (project root). 3 | windows: 4 | - window_name: django-slugify-processor 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.9 2 | python 3.13.3 3.12.10 3.11.12 3.10.17 3.9.22 3.8.20 3.7.17 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 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## django-slugify-processor 1.10.x (unreleased) 4 | 5 | - _Add your latest changes from PRs here_ 6 | 7 | ### Development 8 | 9 | #### chore: Implement PEP 563 deferred annotation resolution (#406) 10 | 11 | - Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking. 12 | - Enable Ruff checks for PEP-compliant annotations: 13 | - [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/) 14 | - [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/) 15 | 16 | For more details on PEP 563, see: https://peps.python.org/pep-0563/ 17 | 18 | ## django-slugify-processor 1.9.0 (2024-12-20) 19 | 20 | _Maintenance only, no bug fixes or features_ 21 | 22 | ### Breaking changes (#405) 23 | 24 | - Drop Python 3.8 25 | 26 | The minimum version of Python in this and future releases is Python 3.9. 27 | 28 | Python 3.8 reached end-of-life status on October 7th, 2024 (see PEP 569). 29 | 30 | ### Development 31 | 32 | - Aggressive automated lint fixes via `ruff` (#404) 33 | 34 | via ruff v0.8.4, all automated lint fixes, including unsafe and previews were applied for Python 3.9: 35 | 36 | ```sh 37 | ruff check --select ALL . --fix --unsafe-fixes --preview --show-fixes; ruff format . 38 | ``` 39 | 40 | ## django-slugify-processor 1.8.0 (2024-11-26) 41 | 42 | _Maintenance only, no bug fixes or features_ 43 | 44 | ### Breaking changes 45 | 46 | #### Project and package management: poetry to uv (#402) 47 | 48 | [uv] is the new package and project manager for the project, replacing Poetry. 49 | 50 | [uv]: https://github.com/astral-sh/uv 51 | 52 | #### Build system: poetry to hatchling (#402) 53 | 54 | [Build system] moved from [poetry] to [hatchling]. 55 | 56 | [Build system]: https://packaging.python.org/en/latest/tutorials/packaging-projects/#choosing-a-build-backend 57 | [poetry]: https://github.com/python-poetry/poetry 58 | [hatchling]: https://hatch.pypa.io/latest/ 59 | 60 | #### Django 3.2 dropped (#397) 61 | 62 | Django 4.2+ is the new minimum version. Support for django 3.2 is dropped. 63 | 64 | v1.7.x is the final Django 3.2 versions. 65 | 66 | Django 3.2's extended support has ended April 1st, 2024 [^DJ3_2_deprecation]. 67 | 68 | [^DJ3_2_deprecation]: Django Project. Unsupported previous releases. https://www.djangoproject.com/download/#unsupported-versions. 69 | 70 | ### Development 71 | 72 | - Support for Django 5.1 in CI, trove classifiers (#398) 73 | 74 | ## django-slugify-processor 1.7.0 (2024-08-17) 75 | 76 | ### Upcoming deprecations 77 | 78 | - 1.7.x is the last branch supporting Django 3.2. 79 | 80 | 1.8+ will drop Django 3.2. 81 | 82 | ### Documentation 83 | 84 | - Automatically linkify links that were previously only text. 85 | 86 | ### Development 87 | 88 | - Aggressive automated lint fixes via `ruff` (#388) 89 | 90 | via ruff v0.3.4, all automated lint fixes, including unsafe and previews were applied: 91 | 92 | ```sh 93 | ruff check --select ALL . --fix --unsafe-fixes --preview --show-fixes; ruff format . 94 | ``` 95 | 96 | Branches were treated with: 97 | 98 | ```sh 99 | git rebase \ 100 | --strategy-option=theirs \ 101 | --exec 'poetry run ruff check --select ALL . --fix --unsafe-fixes --preview --show-fixes; poetry run ruff format .; git add src tests; git commit --amend --no-edit' \ 102 | origin/master 103 | ``` 104 | 105 | - poetry: 1.7.1 -> 1.8.2 106 | 107 | See also: https://github.com/python-poetry/poetry/blob/1.8.2/CHANGELOG.md 108 | 109 | - ruff 0.2.2 -> 0.3.0 (#386) 110 | 111 | Related formattings. Update CI to use `ruff check .` instead of `ruff .`. 112 | 113 | See also: https://github.com/astral-sh/ruff/blob/v0.3.0/CHANGELOG.md 114 | 115 | - Strengthen linting (#385) 116 | 117 | - Add flake8-commas (COM) 118 | 119 | - https://docs.astral.sh/ruff/rules/#flake8-commas-com 120 | - https://pypi.org/project/flake8-commas/ 121 | 122 | - Add flake8-builtins (A) 123 | 124 | - https://docs.astral.sh/ruff/rules/#flake8-builtins-a 125 | - https://pypi.org/project/flake8-builtins/ 126 | 127 | - Add flake8-errmsg (EM) 128 | 129 | - https://docs.astral.sh/ruff/rules/#flake8-errmsg-em 130 | - https://pypi.org/project/flake8-errmsg/ 131 | 132 | ## django-slugify-processor 1.6.0 (2023-12-09) 133 | 134 | _Maintenance only, no bug fixes or features_ 135 | 136 | ### Packaging 137 | 138 | - pyproject.toml: Add django 5.0 (#382) 139 | - pyproject.toml: Remove django 4.0 and 4.1 from pyproject (#382) 140 | 141 | ### Development 142 | 143 | - Remove `django_slugify_processor.settings`, which was unused (#381). 144 | 145 | ### CI 146 | 147 | - Move CodeQL from advanced configuration file to GitHub's default 148 | - ci: Add pydocstyle rule to ruff (#380) 149 | - ci(tests): Remove django 4.0 and 4.1 from testgrid (#382) 150 | - ci(tests): Add django 5.0 to testgrid (#382) 151 | - ci(tests): Only test latest python (#382) 152 | 153 | As django 5.0 drops python 3.8 and 3.9, just use `3.x` to reduce the amount of jobs created in the test matrix (and keep the workflow itself simple). 154 | 155 | ### Documentation 156 | 157 | - Add docstrings to functions, methods, classes, and packages (#380) 158 | 159 | ## django-slugify-processor 1.5.0 (2023-11-18) 160 | 161 | _Maintenance only, no bug fixes or features_ 162 | 163 | ### Development 164 | 165 | - Poetry: 1.6.1 -> 1.7.0 166 | 167 | See also: https://github.com/python-poetry/poetry/blob/1.7.0/CHANGELOG.md 168 | 169 | - Move formatting from `black` to [`ruff format`] (#379) 170 | 171 | This retains the same formatting style of `black` while eliminating a 172 | dev dependency by using our existing rust-based `ruff` linter. 173 | 174 | [`ruff format`]: https://docs.astral.sh/ruff/formatter/ 175 | 176 | - Packaging (poetry): Fix development dependencies 177 | 178 | Per [Poetry's docs on managing dependencies] and `poetry check`, we had it wrong: Instead of using extras, we should create these: 179 | 180 | ```toml 181 | [tool.poetry.group.group-name.dependencies] 182 | dev-dependency = "1.0.0" 183 | ``` 184 | 185 | Which we now do. 186 | 187 | [Poetry's docs on managing dependencies]: https://python-poetry.org/docs/master/managing-dependencies/ 188 | 189 | - Packaging: Add Python 3.12 to trove classifiers 190 | - CI: Update action packages to fix warnings 191 | 192 | - [dorny/paths-filter]: 2.7.0 -> 2.11.1 193 | - [codecov/codecov-action]: 2 -> 3 194 | 195 | [dorny/paths-filter]: https://github.com/dorny/paths-filter 196 | [codecov/codecov-action]: https://github.com/codecov/codecov-action 197 | 198 | ## django-slugify-processor 1.4.0post0 (2023-10-07) 199 | 200 | ### Documentation 201 | 202 | - README updates so links point to new django version 203 | 204 | ## django-slugify-processor 1.4.0 (2023-10-07) 205 | 206 | ### What's new 207 | 208 | - Now [`mypy --strict`] compliant (#376) 209 | 210 | [`mypy --strict`]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict 211 | 212 | ### Packaging 213 | 214 | - Move pytest configuration to `pyproject.toml` (#375) 215 | 216 | ### Breaking changes 217 | 218 | - Drop Python 3.7 (#374) 219 | - Drop Django 2.2, 3.0, 3.1 220 | 221 | Django 3.2+ is now supported 222 | 223 | ## django-slugify-processor 1.3.1 (2023-07-04) 224 | 225 | _Maintenance only, no bug fixes or features_ 226 | 227 | ### Internal improvements 228 | 229 | - Move formatting, import sorting, and linting to [ruff]. 230 | 231 | This rust-based checker has dramatically improved performance. Linting and 232 | formatting can be done almost instantly. 233 | 234 | This change replaces isort, flake8 and flake8 plugins. 235 | 236 | Including additional formatting rules for ruff in #372. 237 | 238 | [ruff]: https://ruff.rs 239 | 240 | ### Infrastructure 241 | 242 | - Bump python to 3.11 243 | - CI speedups (#365) 244 | 245 | - Split out release to separate job so the PyPI Upload docker image isn't pulled on normal runs 246 | - Clean up CodeQL 247 | 248 | - poetry: 1.1.x -> 1.5.0 249 | 250 | See also: 251 | 252 | - 253 | - 254 | - 255 | - 256 | 257 | - Typings: Fix django-stubs settings module issue on CI (#372) 258 | 259 | ### Packaging 260 | 261 | - Remove `.tmuxp-before-script.sh` (was used by `.tmuxp.yaml`'s `before_script`) 262 | - Merge `.coveragerc` into `pyproject.toml` (#366) 263 | - Remove `MANIFEST.in` (#366) 264 | 265 | This is made obsolete via poetry's `include` inside of pyproject.toml. 266 | 267 | ## django-slugify-processor 1.3.0 (2022-09-11) 268 | 269 | ### Documentation 270 | 271 | - Render changelog in [`linkify_issues`] (#360, #355) 272 | - Fix Table of contents rendering with sphinx autodoc with [`sphinx_toctree_autodoc_fix`] (#355) 273 | 274 | [`linkify_issues`]: https://gp-libs.git-pull.com/linkify_issues/ 275 | [`sphinx_toctree_autodoc_fix`]: https://gp-libs.git-pull.com/sphinx_toctree_autodoc_fix/ 276 | 277 | ### Development 278 | 279 | - Add [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) (#361) 280 | - Add [flake8-comprehensions](https://github.com/adamchainz/flake8-comprehensions) (#362) 281 | 282 | ## django-slugify-processor 1.2.0 (2022-08-16) 283 | 284 | ### Development 285 | 286 | Infrastructure updates for static type checking and doctest examples. 287 | 288 | - Move project layout to `src/`, va #364 289 | - Initial [doctests] support added, via #359 290 | 291 | [doctests]: https://docs.python.org/3/library/doctest.html 292 | 293 | - Initial [mypy] validation, via #359 294 | 295 | [mypy]: https://github.com/python/mypy 296 | 297 | - CI (tests, docs): Improve caching of python dependencies via `action/setup-python`'s v3/4's new 298 | poetry caching, via #359 299 | 300 | - CI (docs): Skip if no `PUBLISH` condition triggered, via #359 301 | 302 | - CI: Remove `.pre-commit-config.yaml`, users should know enough to handle these things themselves. 303 | 304 | ## django-slugify-processor 1.1.1 (2022-03-08) 305 | 306 | ### Documentation 307 | 308 | - README URL fixes 309 | 310 | ### Development 311 | 312 | - CI: Remove tox and tox-poetry-installer 313 | 314 | This would break the poetry command when used inside the virtualenv. 315 | 316 | ## django-slugify-processor 1.1.0 (2022-03-08) 317 | 318 | Mainly an update for the new repo URL: https://github.com/tony/django-slugify-processor 319 | 320 | ### Documentation 321 | 322 | - Switch to `furo` theme 323 | 324 | ### Development 325 | 326 | - Run pyupdate @ 3.7 327 | - New repo URL: https://github.com/tony/django-slugify-processor 328 | 329 | ## django-slugify-processor 1.0.0 (2022-01-08) 330 | 331 | ### Compatibility 332 | 333 | - Python 3.6 support dropped (#350) 334 | - Python 3.10 support added (#350) 335 | - Django 4.0 support 336 | - Include tests/ in packages 337 | 338 | ### Development 339 | 340 | - Poetry: Update poetry to 1.1 341 | - CI: Use poetry 1.1.12 and `install-poetry.py` installer (#333 + #350) 342 | - Relock poetry.lock at 1.1 (w/ 1.1.7's fix) 343 | - Add `.pre-commit-config.yaml` (#350) 344 | - Remove unused `requirements.txt`-related files 345 | - `.tool-versions`, `.python-version` 346 | 347 | In an effort to work across multiple versions with `pyenv` and `asdf` with `asdf-python`, set 348 | multiple of the latest patched releases for python if they're supported. 349 | 350 | - `tox` reorganization (#352) 351 | - Add `tox` as a test dependency 352 | - Add [enpaul/tox-poetry-installer](https://github.com/enpaul/tox-poetry-installer) to work with 353 | our project dependencies 354 | - Update tox.ini to use django's new `main` branch, python 3.8 minimum for django's main branch 355 | 356 | ## django-slugify-processor 0.11.0 (2021-06-16) 357 | 358 | - #329: Move to markdown 359 | 360 | ## django-slugify-processor 0.10.0 (2021-06-16) 361 | 362 | - Drop python 2.7, 3.5 and unsupported django versions 363 | - Remove unneeded `utf-8` versions 364 | 365 | ## django-slugify-processor 0.9.0 (2020-08-08) 366 | 367 | - Move doc URL from to git-pull.com 368 | - Move travis -> gh actions 369 | - Move pipenv -> poetry 370 | - Move RTD -> self-hosted docs 371 | - Relax package constraints 372 | - Add package constraints for cases where python version / django effects things 373 | 374 | ## django-slugify-processor 0.8.4 (2017-12-03) 375 | 376 | - Get docs online 377 | - Clean up API docs in code 378 | 379 | ## django-slugify-processor 0.8.3 (2017-12-02) 380 | 381 | - Add test_app, and tests for django-extensions and django-autoslug 382 | 383 | ## django-slugify-processor 0.8.2 (2017-12-02) 384 | 385 | - Try to tweak README / fix on PyPI 386 | 387 | ## django-slugify-processor 0.8.1 (2017-12-01) 388 | 389 | - README updates 390 | - Support for overriding builtin slugify in templates 391 | - Move template filter to _templatetags/slugify_processor.py_ 392 | 393 | ## django-slugify-processor 0.8.0 (2017-11-26) 394 | 395 | - Initial commit 396 | 397 | 400 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017- 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 | WATCH_FILES= find . -type f -not -path '*/\.*' | grep -i '.*[.]py$$' 2> /dev/null 2 | PY_FILES= find . -type f -not -path '*/\.*' -and -not -path '*/settings\/*' -and -not -path '*/migrations\/*' | grep -i '.*[.]py$$' 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 | build_docs: 15 | pushd docs; $(MAKE) html; popd 16 | 17 | watch_docs: 18 | pushd docs; $(MAKE) watch_docs; popd 19 | 20 | start: 21 | $(MAKE) test; uv run ptw . 22 | 23 | test: 24 | uv run py.test $(test) 25 | 26 | watch_test: 27 | if command -v entr > /dev/null; then ${WATCH_FILES} | entr -c $(MAKE) test; else $(MAKE) test entr_warn; fi 28 | 29 | start_docs: 30 | $(MAKE) -C docs start 31 | 32 | design_docs: 33 | $(MAKE) -C docs design 34 | 35 | ruff_format: 36 | uv run ruff format . 37 | 38 | ruff: 39 | uv run ruff check . 40 | 41 | watch_ruff: 42 | if command -v entr > /dev/null; then ${WATCH_FILES} | entr -c $(MAKE) ruff; else $(MAKE) ruff entr_warn; fi 43 | 44 | mypy: 45 | uv run mypy `${PY_FILES}` 46 | 47 | watch_mypy: 48 | if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) mypy; else $(MAKE) mypy entr_warn; fi 49 | 50 | format_markdown: 51 | prettier --parser=markdown -w *.md docs/*.md docs/**/*.md CHANGES 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-slugify-processor 2 | 3 | Custom-[`slugify()`](https://docs.djangoproject.com/en/4.2/ref/utils/#django.utils.text.slugify) 4 | support for django. 5 | 6 | [![Python Package](https://img.shields.io/pypi/v/django-slugify-processor.svg)](https://pypi.org/project/django-slugify-processor/) 7 | [![Build Status](https://github.com/tony/django-slugify-processor/workflows/tests/badge.svg)](https://django-slugify-processor.git-pull.com/) 8 | [![Docs](https://github.com/tony/django-slugify-processor/workflows/docs/badge.svg)](https://github.com/tony/django-slugify-processor/actions?query=workflow%3Adocs) 9 | [![Code Coverage](https://codecov.io/gh/tony/django-slugify-processor/branch/master/graph/badge.svg)](https://codecov.io/gh/tony/django-slugify-processor) 10 | [![License](https://img.shields.io/github/license/tony/django-slugify-processor.svg)](https://github.com/tony/django-slugify-processor/blob/master/LICENSE) 11 | 12 | # What are slugs? 13 | 14 | _Slugs_ are URL's, typically generated from post titles, that you want to be both human readable and 15 | a valid URL. They are SEO friendly. 16 | 17 | Django provides a [`slugify()`](https://docs.djangoproject.com/en/4.2/ref/utils/#django.utils.text.slugify) 18 | function which is also made available as a [default filter](https://github.com/django/django/blob/4.2.6/django/template/defaultfilters.py#L253-L261). 19 | 20 | Django slugs can be automatically generated in django models via packages such as: 21 | 22 | - [django-autoslug](https://pypi.python.org/pypi/django-autoslug) 23 | ([docs](https://pythonhosted.org/django-autoslug/), 24 | [github](https://github.com/neithere/django-autoslug)) 25 | - [django-extensions](https://pypi.python.org/pypi/django-extensions) via 26 | [AutoSlugField](https://django-extensions.readthedocs.io/en/latest/field_extensions.html) 27 | ([docs](https://django-extensions.readthedocs.io/en/latest/), 28 | [github](https://github.com/django-extensions/django-extensions)) 29 | 30 | # The problem 31 | 32 | This project is based on an article from [devel.tech](https://devel.tech) covering 33 | [django's import strings](https://devel.tech/tips/n/djms3tTe/how-django-uses-deferred-imports-to-scale/). 34 | 35 | Corner cases exist with slugification. For instance: 36 | 37 | | Term | [`django.utils.text.slugify`] | What you want | 38 | | ---- | ----------------------------- | ------------- | 39 | | C | c (correct) | n/a | 40 | | C++ | c | cpp | 41 | | C# | c | c-sharp | 42 | 43 | To make matters worse, if using a specialized model field like `AutoSlugField` from django-autoslug 44 | or django-extensions, the default behavior may be to name the slugs for C++ and C# to "c-1", "c-2" 45 | after "c" is taken. 46 | 47 | Here's another case, acronyms / shorthands: 48 | 49 | | Term | [`django.utils.text.slugify`] | What you (may) want | 50 | | ------------------ | ----------------------------- | ------------------- | 51 | | New York City | new-york-city | nyc | 52 | | Y Combinator | y-combinator | yc | 53 | | Portland | portland | pdx | 54 | | Texas | texas | tx | 55 | | $ | '' (empty) | usd, aud, etc? | 56 | | US$ | us | usd | 57 | | A$ | a | aud | 58 | | bitcoin | bitcoin | btc | 59 | | United States | united-states | usa | 60 | | League of Legends | league-of-legends | league | 61 | | Apple® iPod Touch | apple-ipod-touch | ipod-touch | 62 | 63 | Each website and niche has its own edge cases for slugs. So we need a solution that can scale, where 64 | you can craft your own functions. 65 | 66 | # How django-slugify-processor helps 67 | 68 | This builds on top of [`django.utils.text.slugify`] to handle your django project's edgecases. By 69 | default, django-slugify-processor will be a pass through to django's default behavior. Adding 70 | slugification functions via your Django project's settings file allows you to adjust. 71 | 72 | [`django.utils.text.slugify`]: https://github.com/django/django/blob/4.2.6/django/template/defaultfilters.py#L253-L261 73 | 74 | # Installation 75 | 76 | ```console 77 | $ pip install django-slugify-processor 78 | ``` 79 | 80 | # Configure 81 | 82 | To create a processor, create a function that accepts a string, and returns a string. Assume this is 83 | _project/app/slugify_processors.py_: 84 | 85 | ```python 86 | def my_processor(value): 87 | value = value.replace('++', 'pp') 88 | return value 89 | ``` 90 | 91 | Inside of your settings, add a `SLUGIFY_PROCESSORS` list of strings that points to the function. 92 | Anything that's compatible with 93 | [import_string](https://docs.djangoproject.com/en/4.2/ref/utils/#django.utils.module_loading.import_string), 94 | in your settings file: 95 | 96 | ```python 97 | SLUGIFY_PROCESSORS = [ 98 | 'project.app.slugify_processors.my_processor' 99 | ] 100 | ``` 101 | 102 | # Usage 103 | 104 | ## In normal django code 105 | 106 | Import `slugify` from `django_slugify_processor.text`: 107 | 108 | ```python 109 | from django_slugify_processor.text import slugify 110 | 111 | print(slugify('C++')) 112 | > 'cpp' 113 | ``` 114 | 115 | ## Template code 116 | 117 | django-slugify-processor is designed to override the built-in`slugify` filter. 118 | 119 | ### via load 120 | 121 | You can load by default via `{% load django_slugify_processor %}` in your template. 122 | 123 | In your settings `INSTALLED_APPS`: 124 | 125 | ```python 126 | INSTALLED_APPS = [ 127 | 'django_slugify_processor' 128 | ] 129 | ``` 130 | 131 | In your template: 132 | 133 | ```django 134 | {% load slugify_processor %} 135 | {{"C++"|slugify}} 136 | ``` 137 | 138 | ### via built-in 139 | 140 | To make this available in all templates, in the `OPTIONS` of your template engine, add 141 | `django_slugify_processor.template_tags`: 142 | 143 | ```python 144 | TEMPLATES = [{ 145 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 146 | 'OPTIONS': { 147 | 'builtins': [ 148 | 'django_slugify_processor.templatetags.slugify_processor', 149 | ], 150 | }, 151 | }] 152 | ``` 153 | 154 | From within the template file: 155 | 156 | ```django 157 | {{"C++"|slugify}} 158 | ``` 159 | 160 | Output should be: cpp 161 | 162 | ## Models 163 | 164 | For the most up to date documentation, view the documentation for the plugin you're using (e.g. 165 | django-autoslug or django-extensions). 166 | 167 | To use django-slugify-processor's `slugify` instead of django's default, there will be a field 168 | option to use the function. 169 | 170 | ### django-extensions 171 | 172 | Tested with 1.9.7 (2017-11-26): 173 | 174 | ```python 175 | from django.db import models 176 | 177 | from django_extensions.db.fields import AutoSlugField 178 | from django_slugify_processors.text import slugify 179 | 180 | class MyModel(models.Model): 181 | title = models.CharField(max_length=255) 182 | slug = AutoSlugField( 183 | populate_from='title', 184 | slugify_function=slugify 185 | ) 186 | ``` 187 | 188 | ### django-autoslug 189 | 190 | Tested with 1.9.3 (2017-11-26): 191 | 192 | ```python 193 | from django.db import models 194 | 195 | from autoslug import AutoSlugField 196 | from django_slugify_processors.text import slugify 197 | 198 | class MyModel(models.Model): 199 | title = models.CharField(max_length=255) 200 | slug = AutoSlugField( 201 | populate_from='title', 202 | slugify=slugify 203 | ) 204 | ``` 205 | 206 | # Credits 207 | 208 | - tox.ini based off DRF's (BSD 2-clause licensed) 209 | - yapf configuration based off RTD / devel.tech's (MIT-licensed) 210 | 211 | # Project details 212 | 213 | - python support >= 3.9, pypy3 214 | - django support > 4.2, 215 | - Source https://github.com/tony/django-slugify-processor 216 | - Docs https://django-slugify-processor.git-pull.com 217 | - API https://django-slugify-processor.git-pull.com/api.html 218 | - Changelog https://django-slugify-processor.git-pull.com/history.html 219 | - Issues https://github.com/tony/django-slugify-processor/issues 220 | - Test Coverage https://codecov.io/gh/tony/django-slugify-processor 221 | - pypi https://pypi.python.org/pypi/django-slugify-processor 222 | - Open Hub https://www.openhub.net/p/django-slugify-processor 223 | - License MIT 224 | - git repo 225 | 226 | ```bash 227 | $ git clone https://github.com/tony/django-slugify-processor.git 228 | ``` 229 | 230 | ## Development 231 | 232 | Install stable: 233 | 234 | ```console 235 | $ pip install django-slugify-processor 236 | ``` 237 | 238 | Local installation: 239 | 240 | ```console 241 | $ git clone https://github.com/tony/django-slugify-processor.git 242 | ``` 243 | 244 | ```console 245 | $ cd ./django-slugify-processor 246 | ``` 247 | 248 | Test: 249 | 250 | ```console 251 | $ make test 252 | ``` 253 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | SHELL := /bin/bash 4 | WATCH_FILES= find .. -type f -not -path '*/\.*' | grep -i '.*[.]\(rst\|md\)\$\|.*[.]py\$\|CHANGES\|TODO\|.*conf\.py' 2> /dev/null 5 | HTTP_PORT = 8030 6 | 7 | # You can set these variables from the command line. 8 | SPHINXOPTS = 9 | SPHINXBUILD = uv run sphinx-build 10 | PAPER = 11 | BUILDDIR = _build 12 | 13 | # Internal variables. 14 | PAPEROPT_a4 = -D latex_paper_size=a4 15 | PAPEROPT_letter = -D latex_paper_size=letter 16 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 17 | # the i18n builder cannot share the environment and doctrees with the others 18 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | 20 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 21 | 22 | help: 23 | @echo "Please use \`make ' where is one of" 24 | @echo " html to make standalone HTML files" 25 | @echo " dirhtml to make HTML files named index.html in directories" 26 | @echo " singlehtml to make a single large HTML file" 27 | @echo " pickle to make pickle files" 28 | @echo " json to make JSON files" 29 | @echo " htmlhelp to make HTML files and a HTML help project" 30 | @echo " qthelp to make HTML files and a qthelp project" 31 | @echo " devhelp to make HTML files and a Devhelp project" 32 | @echo " epub to make an epub" 33 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 34 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 35 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 36 | @echo " text to make text files" 37 | @echo " man to make manual pages" 38 | @echo " texinfo to make Texinfo files" 39 | @echo " info to make Texinfo files and run them through makeinfo" 40 | @echo " gettext to make PO message catalogs" 41 | @echo " changes to make an overview of all changed/added/deprecated items" 42 | @echo " xml to make Docutils-native XML files" 43 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 44 | @echo " linkcheck to check all external links for integrity" 45 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 46 | 47 | clean: 48 | rm -rf $(BUILDDIR)/* 49 | 50 | html: 51 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 52 | @echo 53 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 54 | 55 | dirhtml: 56 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 59 | 60 | singlehtml: 61 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 62 | @echo 63 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 64 | 65 | pickle: 66 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 67 | @echo 68 | @echo "Build finished; now you can process the pickle files." 69 | 70 | json: 71 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 72 | @echo 73 | @echo "Build finished; now you can process the JSON files." 74 | 75 | htmlhelp: 76 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 77 | @echo 78 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 79 | ".hhp project file in $(BUILDDIR)/htmlhelp." 80 | 81 | qthelp: 82 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 83 | @echo 84 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 85 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 86 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" 87 | @echo "To view the help file:" 88 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" 89 | 90 | devhelp: 91 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 92 | @echo 93 | @echo "Build finished." 94 | @echo "To view the help file:" 95 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" 96 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" 97 | @echo "# devhelp" 98 | 99 | epub: 100 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 101 | @echo 102 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 103 | 104 | latex: 105 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 106 | @echo 107 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 108 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 109 | "(use \`make latexpdf' here to do that automatically)." 110 | 111 | latexpdf: 112 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 113 | @echo "Running LaTeX files through pdflatex..." 114 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 115 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 116 | 117 | latexpdfja: 118 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 119 | @echo "Running LaTeX files through platex and dvipdfmx..." 120 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 121 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 122 | 123 | text: 124 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 125 | @echo 126 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 127 | 128 | man: 129 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 130 | @echo 131 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 132 | 133 | texinfo: 134 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 135 | @echo 136 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 137 | @echo "Run \`make' in that directory to run these through makeinfo" \ 138 | "(use \`make info' here to do that automatically)." 139 | 140 | info: 141 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 142 | @echo "Running Texinfo files through makeinfo..." 143 | make -C $(BUILDDIR)/texinfo info 144 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 145 | 146 | gettext: 147 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 148 | @echo 149 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 150 | 151 | changes: 152 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 153 | @echo 154 | @echo "The overview file is in $(BUILDDIR)/changes." 155 | 156 | linkcheck: 157 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 158 | @echo 159 | @echo "Link check complete; look for any errors in the above output " \ 160 | "or in $(BUILDDIR)/linkcheck/output.txt." 161 | 162 | doctest: 163 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 164 | @echo "Testing of doctests in the sources finished, look at the " \ 165 | "results in $(BUILDDIR)/doctest/output.txt." 166 | 167 | xml: 168 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 169 | @echo 170 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 171 | 172 | pseudoxml: 173 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 174 | @echo 175 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 176 | 177 | redirects: 178 | $(SPHINXBUILD) -b rediraffewritediff $(ALLSPHINXOPTS) $(BUILDDIR)/redirects 179 | @echo 180 | @echo "Build finished. The redirects are in rediraffe_redirects." 181 | 182 | checkbuild: 183 | rm -rf $(BUILDDIR) 184 | $(SPHINXBUILD) -n -q ./ $(BUILDDIR) 185 | 186 | watch: 187 | if command -v entr > /dev/null; then ${WATCH_FILES} | entr -c $(MAKE) html; else $(MAKE) html; fi 188 | 189 | serve: 190 | @echo '=================================================================' 191 | @echo 192 | @echo 'docs server running at http://localhost:${HTTP_PORT}' 193 | @echo 194 | @echo '=================================================================' 195 | @$(MAKE) serve_py3 196 | 197 | serve_py3: 198 | python -m http.server ${HTTP_PORT} --directory _build/html 199 | 200 | dev: 201 | $(MAKE) -j watch serve 202 | 203 | start: 204 | uv run sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --port ${HTTP_PORT} $(O) 205 | 206 | design: 207 | # This adds additional watch directories (for _static file changes) and disable incremental builds 208 | uv run sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --port ${HTTP_PORT} --watch "." -a $(O) 209 | -------------------------------------------------------------------------------- /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-slugify-processor.css: -------------------------------------------------------------------------------- 1 | h1.logo { 2 | font-size: 17.25px; 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-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-256x256.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-36x36.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-384x384.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-48x48.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-72x72.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-chrome-96x96.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /docs/_static/img/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-icon.png -------------------------------------------------------------------------------- /docs/_static/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/favicon-96x96.png -------------------------------------------------------------------------------- /docs/_static/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/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-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/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-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /docs/_static/img/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /docs/_static/img/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /docs/_static/img/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/_static/img/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /docs/_static/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/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-slugify-processor.css']%} 6 | 7 | {%- block extrahead %} 8 | {{ super() }} 9 | {%- if theme_show_meta_manifest_tag == true %} 10 | 11 | {% endif -%} 12 | {%- if theme_show_meta_og_tags == true %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% endif -%} 29 | {%- if theme_show_meta_app_icon_tags == true %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% endif -%} 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /docs/_templates/more.html: -------------------------------------------------------------------------------- 1 | 2 | Fork me on GitHub 5 | 6 | -------------------------------------------------------------------------------- /docs/_templates/sidebar/projects.html: -------------------------------------------------------------------------------- 1 | 56 | 70 | -------------------------------------------------------------------------------- /docs/_templates/star.html: -------------------------------------------------------------------------------- 1 |

2 | 4 |

5 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | (api)= 2 | 3 | # API Reference 4 | 5 | ## Slugify function 6 | 7 | ```{eval-rst} 8 | .. autofunction:: django_slugify_processor.text.slugify 9 | ``` 10 | 11 | ## Template tag 12 | 13 | ```{eval-rst} 14 | .. autofunction:: 15 | django_slugify_processor.templatetags.slugify_processor.slugify 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Sphinx configuration for Django Slugify Processor.""" 2 | 3 | # flake8: NOQA: E501 4 | from __future__ import annotations 5 | 6 | import inspect 7 | import pathlib 8 | import sys 9 | import typing as t 10 | from os.path import relpath 11 | 12 | import django_slugify_processor 13 | 14 | if t.TYPE_CHECKING: 15 | from sphinx.application import Sphinx 16 | 17 | # Get the project root dir, which is the parent dir of this 18 | cwd = pathlib.Path(__file__).parent 19 | project_root = cwd.parent 20 | src_root = project_root / "src" 21 | 22 | sys.path.insert(0, str(src_root)) 23 | sys.path.insert(0, str(cwd / "_ext")) 24 | 25 | # package data 26 | about: dict[str, str] = {} 27 | with (src_root / "django_slugify_processor" / "__about__.py").open() as fp: 28 | exec(fp.read(), about) 29 | 30 | extensions = [ 31 | "sphinx.ext.autodoc", 32 | "sphinx_autodoc_typehints", 33 | "sphinx.ext.intersphinx", 34 | "sphinx.ext.linkcode", 35 | "sphinx.ext.napoleon", 36 | "sphinx_inline_tabs", 37 | "sphinx_copybutton", 38 | "sphinxext.opengraph", 39 | "sphinxext.rediraffe", 40 | "myst_parser", 41 | "linkify_issues", 42 | ] 43 | myst_enable_extensions = [ 44 | "colon_fence", 45 | "substitution", 46 | "replacements", 47 | "strikethrough", 48 | "linkify", 49 | ] 50 | 51 | templates_path = ["_templates"] 52 | 53 | source_suffix = {".rst": "restructuredtext", ".md": "markdown"} 54 | 55 | master_doc = "index" 56 | 57 | project = about["__title__"] 58 | project_copyright = about["__copyright__"] 59 | 60 | version = "{}".format(".".join(about["__version__"].split("."))[:2]) 61 | release = "{}".format(about["__version__"]) 62 | 63 | exclude_patterns = ["_build"] 64 | 65 | pygments_style = "monokai" 66 | pygments_dark_style = "monokai" 67 | 68 | html_favicon = "_static/favicon.ico" 69 | html_static_path = ["_static"] 70 | html_css_files = ["css/custom.css"] 71 | html_extra_path = ["manifest.json"] 72 | html_theme = "furo" 73 | html_theme_path: list[str] = [] 74 | html_theme_options: dict[str, str | list[dict[str, str]]] = { 75 | "light_logo": "img/icons/logo.svg", 76 | "dark_logo": "img/icons/logo-dark.svg", 77 | "footer_icons": [ 78 | { 79 | "name": "GitHub", 80 | "url": about["__github__"], 81 | "html": """ 82 | 83 | 84 | 85 | """, 86 | "class": "", 87 | }, 88 | ], 89 | "source_repository": f"{about['__github__']}/", 90 | "source_branch": "master", 91 | "source_directory": "docs/", 92 | } 93 | html_sidebars = { 94 | "**": [ 95 | "sidebar/scroll-start.html", 96 | "sidebar/brand.html", 97 | "sidebar/search.html", 98 | "sidebar/navigation.html", 99 | "sidebar/projects.html", 100 | "sidebar/scroll-end.html", 101 | ], 102 | } 103 | 104 | # linkify_issues 105 | issue_url_tpl = about["__github__"] + "/issues/{issue_id}" 106 | 107 | # sphinx.ext.autodoc 108 | toc_object_entries_show_parents = "hide" 109 | 110 | # sphinxext.opengraph 111 | ogp_site_url = about["__docs__"] 112 | ogp_image = "_static/img/icons/icon-192x192.png" 113 | ogp_site_name = about["__title__"] 114 | 115 | # sphinx-copybutton 116 | copybutton_prompt_text = ( 117 | r">>> |\.\.\. |> |\$ |\# | In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " 118 | ) 119 | copybutton_prompt_is_regexp = True 120 | copybutton_remove_prompts = True 121 | 122 | # sphinxext-rediraffe 123 | rediraffe_redirects = "redirects.txt" 124 | rediraffe_branch = "master~1" 125 | 126 | intersphinx_mapping = { 127 | "py": ("https://docs.python.org", None), 128 | "django": ( 129 | "https://docs.djangoproject.com/en/4.2/", 130 | "https://docs.djangoproject.com/en/4.2/_objects/", 131 | ), 132 | } 133 | 134 | 135 | def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str: 136 | """ 137 | Determine the URL corresponding to Python object. 138 | 139 | Notes 140 | ----- 141 | From https://github.com/numpy/numpy/blob/v1.15.1/doc/source/conf.py, 7c49cfa 142 | on Jul 31. License BSD-3. https://github.com/numpy/numpy/blob/v1.15.1/LICENSE.txt 143 | """ 144 | if domain != "py": 145 | return None 146 | 147 | modname = info["module"] 148 | fullname = info["fullname"] 149 | 150 | submod = sys.modules.get(modname) 151 | if submod is None: 152 | return None 153 | 154 | obj = submod 155 | for part in fullname.split("."): 156 | try: 157 | obj = getattr(obj, part) 158 | except Exception: # noqa: PERF203 159 | return None 160 | 161 | # strip decorators, which would resolve to the source of the decorator 162 | # possibly an upstream bug in getsourcefile, bpo-1764286 163 | try: 164 | unwrap = inspect.unwrap 165 | except AttributeError: 166 | pass 167 | else: 168 | if callable(obj): 169 | obj = unwrap(obj) 170 | 171 | try: 172 | fn = inspect.getsourcefile(obj) 173 | except Exception: 174 | fn = None 175 | if not fn: 176 | return None 177 | 178 | try: 179 | source, lineno = inspect.getsourcelines(obj) 180 | except Exception: 181 | lineno = None 182 | 183 | linespec = f"#L{lineno}-L{lineno + len(source) - 1}" if lineno else "" 184 | 185 | fn = relpath(fn, start=pathlib.Path(django_slugify_processor.__file__).parent) 186 | 187 | if "dev" in about["__version__"]: 188 | return "{}/blob/master/{}/{}/{}{}".format( 189 | about["__github__"], 190 | "src", 191 | about["__package_name__"], 192 | fn, 193 | linespec, 194 | ) 195 | return "{}/blob/v{}/{}/{}/{}{}".format( 196 | about["__github__"], 197 | about["__version__"], 198 | "src", 199 | about["__package_name__"], 200 | fn, 201 | linespec, 202 | ) 203 | 204 | 205 | def remove_tabs_js(app: Sphinx, exc: Exception) -> None: 206 | """Fix for sphinx-inline-tabs#18.""" 207 | if app.builder.format == "html" and not exc: 208 | tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js" 209 | tabs_js.unlink(missing_ok=True) 210 | 211 | 212 | def setup(app: Sphinx) -> None: 213 | """Sphinx setup hook.""" 214 | app.connect("build-finished", remove_tabs_js) 215 | -------------------------------------------------------------------------------- /docs/developing.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | [uv] is a required package to develop. 4 | 5 | Install and [git] and [uv] 6 | 7 | Clone: 8 | 9 | ```console 10 | $ git clone https://github.com/tony/django-slugify-processor.git 11 | ``` 12 | 13 | ```console 14 | $ cd django-slugify-processor 15 | ``` 16 | 17 | Install packages: 18 | 19 | ```console 20 | $ uv sync --all-extras --dev 21 | ``` 22 | 23 | [installation documentation]: https://docs.astral.sh/uv/getting-started/installation/ 24 | [git]: https://git-scm.com/ 25 | 26 | ## Tests 27 | 28 | `uv run py.test` 29 | 30 | Helpers: `make test` 31 | 32 | ## Automatically run tests on file save 33 | 34 | 1. `make start` (via [pytest-watcher]) 35 | 2. `make watch_test` (requires installing [entr(1)]) 36 | 37 | [pytest-watcher]: https://github.com/olzhasar/pytest-watcher 38 | 39 | ## Documentation 40 | 41 | Default preview server: http://localhost:8034 42 | 43 | [sphinx-autobuild] will automatically build the docs, watch for file changes and launch a server. 44 | 45 | From home directory: `make start_docs` 46 | From inside `docs/`: `make start` 47 | 48 | [sphinx-autobuild]: https://github.com/executablebooks/sphinx-autobuild 49 | 50 | ### Manual documentation (the hard way) 51 | 52 | `cd docs/` and `make html` to build. `make serve` to start http server. 53 | 54 | Helpers: 55 | `make build_docs`, `make serve_docs` 56 | 57 | Rebuild docs on file change: `make watch_docs` (requires [entr(1)]) 58 | 59 | Rebuild docs and run server via one terminal: `make dev_docs` (requires above, and a 60 | `make(1)` with `-J` support, e.g. GNU Make) 61 | 62 | ## Formatting / Linting 63 | 64 | ### ruff 65 | 66 | The project uses [ruff] to handle formatting, sorting imports and linting. 67 | 68 | ````{tab} Command 69 | 70 | uv: 71 | 72 | ```console 73 | $ uv run ruff 74 | ``` 75 | 76 | If you setup manually: 77 | 78 | ```console 79 | $ ruff check . 80 | ``` 81 | 82 | ```` 83 | 84 | ````{tab} make 85 | 86 | ```console 87 | $ make ruff 88 | ``` 89 | 90 | ```` 91 | 92 | ````{tab} Watch 93 | 94 | ```console 95 | $ make watch_ruff 96 | ``` 97 | 98 | requires [`entr(1)`]. 99 | 100 | ```` 101 | 102 | ````{tab} Fix files 103 | 104 | uv: 105 | 106 | ```console 107 | $ uv run ruff check . --fix 108 | ``` 109 | 110 | If you setup manually: 111 | 112 | ```console 113 | $ ruff check . --fix 114 | ``` 115 | 116 | ```` 117 | 118 | #### ruff format 119 | 120 | [ruff format] is used for formatting. 121 | 122 | ````{tab} Command 123 | 124 | uv: 125 | 126 | ```console 127 | $ uv run ruff format . 128 | ``` 129 | 130 | If you setup manually: 131 | 132 | ```console 133 | $ ruff format . 134 | ``` 135 | 136 | ```` 137 | 138 | ````{tab} make 139 | 140 | ```console 141 | $ make ruff_format 142 | ``` 143 | 144 | ```` 145 | 146 | ### mypy 147 | 148 | [mypy] is used for static type checking. 149 | 150 | ````{tab} Command 151 | 152 | uv: 153 | 154 | ```console 155 | $ uv run mypy . 156 | ``` 157 | 158 | If you setup manually: 159 | 160 | ```console 161 | $ mypy . 162 | ``` 163 | 164 | ```` 165 | 166 | ````{tab} make 167 | 168 | ```console 169 | $ make mypy 170 | ``` 171 | 172 | ```` 173 | 174 | ````{tab} Watch 175 | 176 | ```console 177 | $ make watch_mypy 178 | ``` 179 | 180 | requires [`entr(1)`]. 181 | ```` 182 | 183 | ## Releasing 184 | 185 | [uv] handles virtualenv creation, package requirements, versioning, 186 | building, and publishing. Therefore there is no setup.py or requirements files. 187 | 188 | Update `__version__` in `__about__.py` and `pyproject.toml`:: 189 | 190 | git commit -m 'build(django-slugify-processor): Tag v0.1.1' 191 | git tag v0.1.1 192 | git push 193 | git push --tags 194 | 195 | [uv]: https://github.com/astral-sh/uv 196 | [entr(1)]: http://eradman.com/entrproject/ 197 | [`entr(1)`]: http://eradman.com/entrproject/ 198 | [ruff format]: https://docs.astral.sh/ruff/formatter/ 199 | [ruff]: https://ruff.rs 200 | [mypy]: http://mypy-lang.org/ 201 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | (changes)= 2 | (changelog)= 3 | (history)= 4 | 5 | ```{currentmodule} django_slugify_processor 6 | 7 | ``` 8 | 9 | ```{include} ../CHANGES 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide-toc: true 3 | --- 4 | 5 | (index)= 6 | 7 | ```{include} ../README.md 8 | 9 | ``` 10 | 11 | ```{toctree} 12 | :hidden: 13 | 14 | api 15 | ``` 16 | 17 | ```{toctree} 18 | :caption: Project 19 | :hidden: 20 | 21 | developing 22 | history 23 | GitHub 24 | ``` 25 | -------------------------------------------------------------------------------- /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-slugify-processor", 3 | "short_name": "django-slugify-processor", 4 | "description": "pipeline for slugification edgecases in django", 5 | "theme_color": "#2196f3", 6 | "background_color": "#fff", 7 | "display": "browser", 8 | "Scope": "https://django-slugify-processor.git-pull.com/", 9 | "start_url": "https://django-slugify-processor.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/redirects.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/docs/redirects.txt -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-slugify-processor" 3 | version = "1.9.0" 4 | description = "pipeline for slugification edgecases in django" 5 | requires-python = ">=3.9,<4.0" 6 | license = { text = "MIT" } 7 | authors = [ 8 | {name = "Tony Narlock", email = "tony@git-pull.com"} 9 | ] 10 | classifiers = [ 11 | 'Development Status :: 5 - Production/Stable', 12 | 'Environment :: Web Environment', 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 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.9', 23 | 'Programming Language :: Python :: 3.10', 24 | 'Programming Language :: Python :: 3.11', 25 | 'Programming Language :: Python :: 3.12', 26 | 'Programming Language :: Python :: 3.13', 27 | 'Programming Language :: Python :: Implementation :: CPython', 28 | 'Programming Language :: Python :: Implementation :: PyPy', 29 | 'Topic :: Utilities', 30 | "Typing :: Typed", 31 | ] 32 | readme = 'README.md' 33 | packages = [ 34 | { include = "*", from = "src" }, 35 | ] 36 | include = [ 37 | { path = "tests", format = "sdist" }, 38 | ] 39 | keywords = ["django", "slug", "text"] 40 | homepage = "https://django-slugify-processor.git-pull.com" 41 | dependencies = [ 42 | "django>=4.2" 43 | ] 44 | 45 | [project.urls] 46 | "Bug Tracker" = "https://github.com/tony/django-slugify-processor/issues" 47 | Documentation = "https://django-slugify-processor.git-pull.com" 48 | Repository = "https://github.com/tony/django-slugify-processor" 49 | Changes = "https://github.com/tony/django-slugify-processor/blob/master/CHANGES" 50 | 51 | [project.optional-dependencies] 52 | django-extensions = [ 53 | "django-extensions" 54 | ] 55 | 56 | [tool.uv] 57 | dev-dependencies = [ 58 | # Docs 59 | "sphinx", 60 | "furo", 61 | "gp-libs", 62 | "sphinx-autobuild", 63 | "sphinx-autodoc-typehints", 64 | "sphinx-inline-tabs", 65 | "sphinxext-opengraph", 66 | "sphinx-copybutton", 67 | "sphinxext-rediraffe", 68 | "myst-parser", 69 | "linkify-it-py", 70 | # Testing 71 | "gp-libs", 72 | "pytest", 73 | "pytest-rerunfailures", 74 | "pytest-mock", 75 | "pytest-watcher", 76 | "pytest-django", 77 | "django-extensions", 78 | # Coverage 79 | "codecov", 80 | "coverage", 81 | "pytest-cov", 82 | # Lint 83 | "ruff", 84 | "mypy", 85 | "django-stubs", 86 | ] 87 | 88 | [dependency-groups] 89 | docs = [ 90 | "sphinx", 91 | "furo", 92 | "gp-libs", 93 | "sphinx-autobuild", 94 | "sphinx-autodoc-typehints", 95 | "sphinx-inline-tabs", 96 | "sphinxext-opengraph", 97 | "sphinx-copybutton", 98 | "sphinxext-rediraffe", 99 | "myst-parser", 100 | "linkify-it-py", 101 | ] 102 | testing = [ 103 | "gp-libs", 104 | "pytest", 105 | "pytest-rerunfailures", 106 | "pytest-mock", 107 | "pytest-watcher", 108 | "pytest-django", 109 | "django-extensions", 110 | ] 111 | coverage =[ 112 | "codecov", 113 | "coverage", 114 | "pytest-cov", 115 | ] 116 | lint = [ 117 | "ruff", 118 | "mypy", 119 | "django-stubs", 120 | ] 121 | 122 | [build-system] 123 | requires = ["hatchling"] 124 | build-backend = "hatchling.build" 125 | 126 | [tool.mypy] 127 | plugins = ["mypy_django_plugin.main"] 128 | strict = true 129 | python_version = "3.9" 130 | files = [ 131 | "src/", 132 | "tests/", 133 | ] 134 | 135 | [tool.django-stubs] 136 | django_settings_module = "tests.settings" 137 | 138 | [[tool.mypy.overrides]] 139 | module = [ 140 | "django_extensions.*", 141 | ] 142 | ignore_missing_imports = true 143 | 144 | [tool.coverage.run] 145 | branch = true 146 | parallel = true 147 | omit = [ 148 | "*/_compat.py", 149 | "docs/conf.py", 150 | ] 151 | 152 | [tool.coverage.report] 153 | show_missing = true 154 | skip_covered = true 155 | exclude_lines = [ 156 | "pragma: no cover", 157 | "def __repr__", 158 | "raise NotImplementedError", 159 | "return NotImplemented", 160 | "def parse_args", 161 | "if TYPE_CHECKING:", 162 | "if t.TYPE_CHECKING:", 163 | "@overload( |$)", 164 | "from __future__ import annotations", 165 | ] 166 | 167 | [tool.ruff] 168 | target-version = "py39" 169 | 170 | [tool.ruff.lint] 171 | select = [ 172 | "E", # pycodestyle 173 | "F", # pyflakes 174 | "I", # isort 175 | "UP", # pyupgrade 176 | "A", # flake8-builtins 177 | "B", # flake8-bugbear 178 | "C4", # flake8-comprehensions 179 | "COM", # flake8-commas 180 | "EM", # flake8-errmsg 181 | "Q", # flake8-quotes 182 | "PTH", # flake8-use-pathlib 183 | "SIM", # flake8-simplify 184 | "TRY", # Trycertatops 185 | "PERF", # Perflint 186 | "RUF", # Ruff-specific rules 187 | "D", # pydocstyle 188 | "FA100", # future annotations 189 | ] 190 | ignore = [ 191 | "COM812", # missing trailing comma, ruff format conflict 192 | ] 193 | extend-safe-fixes = [ 194 | "UP006", 195 | "UP007", 196 | ] 197 | pyupgrade.keep-runtime-typing = false 198 | 199 | [tool.ruff.lint.isort] 200 | known-first-party = [ 201 | "django_slugify_processor", 202 | ] 203 | combine-as-imports = true 204 | required-imports = [ 205 | "from __future__ import annotations", 206 | ] 207 | 208 | [tool.ruff.lint.pydocstyle] 209 | convention = "numpy" 210 | 211 | [tool.ruff.lint.per-file-ignores] 212 | "*/__init__.py" = ["F401"] 213 | 214 | [tool.pytest.ini_options] 215 | addopts = "--tb=short --no-header --showlocals --doctest-modules" 216 | doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE" 217 | testpaths = [ 218 | "src/django_slugify_processor", 219 | "tests", 220 | "docs", 221 | ] 222 | pythonpath = ". tests" 223 | filterwarnings = [ 224 | "ignore:The frontend.Option(Parser)? class.*:DeprecationWarning::", 225 | ] 226 | 227 | # pytest-django 228 | django_find_project = false 229 | DJANGO_SETTINGS_MODULE = "tests.settings" 230 | -------------------------------------------------------------------------------- /src/django_slugify_processor/__about__.py: -------------------------------------------------------------------------------- 1 | """Metadata for django-slugify-processor.""" 2 | 3 | from __future__ import annotations 4 | 5 | __title__ = "django-slugify-processor" 6 | __package_name__ = "django_slugify_processor" 7 | __version__ = "1.9.0" 8 | __description__ = "pipeline for slugification edgecases in django" 9 | __email__ = "tony@git-pull.com" 10 | __pypi__ = "https://pypi.python.org/pypi/django-slugify-processor" 11 | __github__ = "https://github.com/tony/django-slugify-processor/" 12 | __docs__ = "https://django-slugify-processor.git-pull.com" 13 | __tracker__ = "https://github.com/tony/django-slugify-processor/issues" 14 | __author__ = "Tony Narlock" 15 | __license__ = "MIT" 16 | __copyright__ = "Copyright 2017- Tony Narlock" 17 | -------------------------------------------------------------------------------- /src/django_slugify_processor/__init__.py: -------------------------------------------------------------------------------- 1 | """django-slugify-processor core package.""" 2 | -------------------------------------------------------------------------------- /src/django_slugify_processor/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tony/django-slugify-processor/afa6278429c45dfbfbf41e78d4206b5be96165fe/src/django_slugify_processor/py.typed -------------------------------------------------------------------------------- /src/django_slugify_processor/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | """Template tags for django-slugify-processor.""" 2 | -------------------------------------------------------------------------------- /src/django_slugify_processor/templatetags/slugify_processor.py: -------------------------------------------------------------------------------- 1 | """Slugify django template filter.""" 2 | 3 | from __future__ import annotations 4 | 5 | from django import template 6 | from django.template.defaultfilters import stringfilter 7 | 8 | from django_slugify_processor.text import slugify as _slugify 9 | 10 | register = template.Library() 11 | 12 | 13 | @register.filter(is_safe=True) 14 | @stringfilter 15 | def slugify(value: str) -> str: 16 | """Template filter override for Django's :func:`django.utils.text.slugify()`. 17 | 18 | Can be installed via a builtin, or via ``{% load slugify_processor %}``. 19 | 20 | Parameters 21 | ---------- 22 | value : str 23 | A value such as an article name or page title, to "slugify", (turn into a 24 | clean, URL-friendly short name) 25 | 26 | Returns 27 | ------- 28 | str 29 | Clean, URL-friendly short name (a.k.a. "slug") of a string (e.g. a page or 30 | article name). 31 | 32 | Examples 33 | -------- 34 | Usage in a Django template: 35 | 36 | .. code-block:: django 37 | 38 | {% load slugify_processor %} 39 | {{ variable|slugify }} 40 | {{ "C++"|slugify }} 41 | """ 42 | return _slugify(value) 43 | -------------------------------------------------------------------------------- /src/django_slugify_processor/text.py: -------------------------------------------------------------------------------- 1 | """Core functionality for django-slugify-processor.""" 2 | 3 | from __future__ import annotations 4 | 5 | from django.conf import settings 6 | from django.utils.module_loading import import_string 7 | from django.utils.text import slugify as django_slugify 8 | 9 | 10 | def slugify(value: str, allow_unicode: bool = False) -> str: 11 | """Override default slugify in django to handle custom scenarios. 12 | 13 | Run value through functions declared in ``SLUGIFY_PROCESSORS`` setting. The value is 14 | then passed-through to Django's :func:`django.utils.text.slugify()`. 15 | 16 | Parameters 17 | ---------- 18 | value : str 19 | A value such as an article name or page title, to "slugify", (turn into a 20 | clean, URL-friendly short name) 21 | allow_unicode : bool 22 | Whether or not to allow unicode (e.g. chinese). 23 | 24 | Returns 25 | ------- 26 | str 27 | Clean, URL-friendly short name (a.k.a. "slug") of a string (e.g. a page or 28 | article name). 29 | 30 | Examples 31 | -------- 32 | Examples of slugify processors, assume *project/app/slugify_processors.py*: 33 | 34 | .. code-block:: python 35 | 36 | def slugify_programming_languages(value): 37 | value = value.lower() 38 | 39 | value = value.replace('c++', 'cpp') 40 | value = value.replace('c#', 'c-sharp') 41 | return value 42 | 43 | def slugify_geo_acronyms(value): 44 | value = value.lower() 45 | 46 | value = value.replace('New York City', 'nyc') 47 | value = value.replace('United States', 'usa') 48 | return value 49 | 50 | def slugify_us_currency(value): 51 | value = value.lower() 52 | 53 | value = value.replace('$', 'usd') 54 | value = value.replace('US$', 'usd') 55 | value = value.replace('US Dollar', 'usd') 56 | value = value.replace('U.S. Dollar', 'usd') 57 | return value 58 | 59 | Settings: 60 | 61 | .. code-block:: python 62 | 63 | SLUGIFY_PROCESSORS = [ 64 | 'project.app.slugify_programming_languages', 65 | 'project.app.slugify_geo_acronyms', 66 | 'project.app.slugify_us_currency', 67 | ] 68 | """ 69 | slugify_processors = getattr(settings, "SLUGIFY_PROCESSORS", []) 70 | for slugify_fn_str in slugify_processors: 71 | slugify_fn_ = import_string(slugify_fn_str) 72 | value = slugify_fn_(value) 73 | 74 | return django_slugify(value, allow_unicode) 75 | -------------------------------------------------------------------------------- /test_app/__init__.py: -------------------------------------------------------------------------------- 1 | """Test django application for django-slugify-processor.""" 2 | -------------------------------------------------------------------------------- /test_app/coding.py: -------------------------------------------------------------------------------- 1 | """Example slug function for django-slugify-processor tests.""" 2 | 3 | from __future__ import annotations 4 | 5 | 6 | def slugify_programming(value: str) -> str: 7 | """Replace c++ with cpp (URL safe).""" 8 | return value.replace("c++", "cpp") 9 | -------------------------------------------------------------------------------- /test_app/models.py: -------------------------------------------------------------------------------- 1 | """Django models for django-slugify-processor test app.""" 2 | 3 | from __future__ import annotations 4 | 5 | import django_extensions.db.fields 6 | from django.db import models 7 | 8 | from django_slugify_processor.text import slugify 9 | 10 | 11 | class MyModel(models.Model): 12 | """Test model for django-slugify-processor.""" 13 | 14 | title = models.CharField(max_length=255) 15 | django_extensions_slug = django_extensions.db.fields.AutoSlugField( 16 | populate_from="title", 17 | slugify_function=slugify, 18 | ) 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test package for django-slugify-processor.""" 2 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """Django settings module for django-slugify-processor tests.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import typing as t 7 | 8 | SECRET_KEY: str = os.getenv("SECRET_KEY", "dummy") 9 | 10 | DATABASES: dict[str, t.Any] = { 11 | "default": {"NAME": ":memory:", "ENGINE": "django.db.backends.sqlite3"}, 12 | } 13 | 14 | INSTALLED_APPS: list[str] = ["test_app"] 15 | 16 | USE_TZ: bool = False 17 | -------------------------------------------------------------------------------- /tests/test_filters.py: -------------------------------------------------------------------------------- 1 | """Test package for django-slugify-processor filters.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django.template import Context, Template 8 | 9 | 10 | def test_slugify_via_builtin_override(settings: t.Any) -> None: 11 | """Tests for overriding of django template builtins.""" 12 | settings.SLUGIFY_PROCESSORS = ["test_app.coding.slugify_programming"] 13 | settings.TEMPLATES = [ 14 | { 15 | "BACKEND": "django.template.backends.django.DjangoTemplates", 16 | "APP_DIRS": True, 17 | "OPTIONS": { 18 | "builtins": ["django_slugify_processor.templatetags.slugify_processor"], 19 | }, 20 | }, 21 | ] 22 | 23 | template = Template("{{'c++'|slugify}}") 24 | assert template.render(Context({})) == "cpp" 25 | 26 | 27 | def test_slugify_via_load_templatetags(settings: t.Any) -> None: 28 | """Tests for using django-slugify-processor via loading template tag.""" 29 | settings.SLUGIFY_PROCESSORS = ["test_app.coding.slugify_programming"] 30 | settings.INSTALLED_APPS = ["django_slugify_processor"] 31 | settings.TEMPLATES = [ 32 | { 33 | "BACKEND": "django.template.backends.django.DjangoTemplates", 34 | "APP_DIRS": True, 35 | }, 36 | ] 37 | 38 | template = Template('{% load slugify_processor %}{{"c++"|slugify}}') 39 | assert template.render(Context({})) == "cpp" 40 | -------------------------------------------------------------------------------- /tests/test_test_project.py: -------------------------------------------------------------------------------- 1 | """Tests using django test app for django-slugify-processor.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | import pytest 8 | from django.apps import apps 9 | 10 | 11 | @pytest.mark.django_db 12 | def test_models_passthrough(settings: t.Any) -> None: 13 | """django-slugify-processor defaults to django's behavior if not set.""" 14 | MyModel = apps.get_model("test_app.MyModel") 15 | entered = "c++" 16 | expected = "c" 17 | 18 | m = MyModel(title=entered) 19 | m.save() 20 | 21 | assert m.django_extensions_slug == expected 22 | 23 | 24 | @pytest.mark.django_db 25 | def test_models(settings: t.Any) -> None: 26 | """django-slugify-processor uses SLUGIFY_PROCESSORS if set.""" 27 | settings.SLUGIFY_PROCESSORS = ["test_app.coding.slugify_programming"] 28 | MyModel = apps.get_model("test_app.MyModel") 29 | entered = "c++" 30 | expected = "cpp" 31 | 32 | m = MyModel(title=entered) 33 | m.save() 34 | 35 | assert m.django_extensions_slug == expected 36 | -------------------------------------------------------------------------------- /tests/test_text.py: -------------------------------------------------------------------------------- 1 | """Tests for django-slugify-processor's text utilities.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing as t 6 | 7 | from django.utils.text import slugify as django_slugify 8 | 9 | from django_slugify_processor.text import slugify 10 | 11 | 12 | def test_slugify_fallback_to_default() -> None: 13 | """Assert slugify() falls back to django's behavior.""" 14 | assert slugify("c++") == django_slugify("c++") 15 | 16 | 17 | def test_slugify_slugify_processors(settings: t.Any) -> None: 18 | """Assert slugify() follows SLUGIFY_PROCESSORS.""" 19 | settings.SLUGIFY_PROCESSORS = ["test_app.coding.slugify_programming"] 20 | assert slugify("c++") == "cpp" 21 | --------------------------------------------------------------------------------