mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-08-03 14:48:17 +00:00
first release
This commit is contained in:
parent
c38c990b5b
commit
4c4d00d4df
2269 changed files with 323498 additions and 2 deletions
43
.github/workflows/deploy_docs.yml
vendored
Normal file
43
.github/workflows/deploy_docs.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
name: "Deploy Documentation"
|
||||||
|
run-name: "Deploy Documentation (@${{ github.actor }})"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
RAILWAY_PROJECT_TOKEN:
|
||||||
|
required: true
|
||||||
|
RAILWAY_SERVICE:
|
||||||
|
required: true
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'docs/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy_docs:
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_PROJECT_TOKEN }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
|
||||||
|
- name: Install Documentation Dependencies
|
||||||
|
run: |
|
||||||
|
cd docs/docs_project
|
||||||
|
npm install
|
||||||
|
|
||||||
|
- name: Install Railway CLI
|
||||||
|
run: npm install -g @railway/cli
|
||||||
|
|
||||||
|
- name: Deploy Documentation
|
||||||
|
run: |
|
||||||
|
cp -r django_cotton docs/docs_project
|
||||||
|
railway up --service=${{ secrets.RAILWAY_SERVICE }}
|
48
.github/workflows/publish_to_pypi.yml
vendored
Normal file
48
.github/workflows/publish_to_pypi.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
name: "Publish to PyPI"
|
||||||
|
run-name: "Publish to PyPI (@${{ github.actor }})"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '!docs/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish_pypi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Install required libraries
|
||||||
|
run: python3 -m pip install toml
|
||||||
|
|
||||||
|
- name: Update Version in pyproject.toml
|
||||||
|
run: |
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
python -c "import toml; f = open('./pyproject.toml', 'r'); c = toml.load(f); f.close(); v = list(map(int, c['tool']['poetry']['version'].split('.'))); v[-1] += 1; c['tool']['poetry']['version'] = '.'.join(map(str, v)); f = open('./pyproject.toml', 'w'); toml.dump(c, f); f.close();"
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git add -A
|
||||||
|
git commit -m "Automatic version bump" --allow-empty
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
- name: Build and publish to PyPI
|
||||||
|
uses: JRubics/poetry-publish@v1.17
|
||||||
|
with:
|
||||||
|
pypi_token: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
|
||||||
|
trigger_deploy_docs:
|
||||||
|
needs: publish_pypi
|
||||||
|
uses: ./.github/workflows/deploy_docs.yml
|
||||||
|
secrets:
|
||||||
|
RAILWAY_PROJECT_TOKEN: ${{ secrets.RAILWAY_PROJECT_TOKEN }}
|
||||||
|
RAILWAY_SERVICE: ${{ secrets.RAILWAY_SERVICE }}
|
30
.github/workflows/test.yml
vendored
Normal file
30
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
docker build -f dev/docker/Dockerfile -t cotton-test-app dev/example_project
|
||||||
|
|
||||||
|
- name: Start Container
|
||||||
|
run: |
|
||||||
|
docker-compose -f dev/docker/docker-compose.yaml up -d
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: docker exec -t cotton-dev-app python manage.py test
|
||||||
|
|
||||||
|
- name: Stop and Remove Services
|
||||||
|
run: docker-compose -f dev/docker/docker-compose.yaml down
|
115
.gitignore
vendored
Normal file
115
.gitignore
vendored
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# Django #
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
db.sqlite3
|
||||||
|
media
|
||||||
|
**/static/app.css
|
||||||
|
|
||||||
|
# Backup files #
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Python #
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
.pytest_cache/
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery
|
||||||
|
celerybeat-schedule.*
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Sublime Text #
|
||||||
|
*.tmlanguage.cache
|
||||||
|
*.tmPreferences.cache
|
||||||
|
*.stTheme.cache
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# sftp configuration file
|
||||||
|
sftp-config.json
|
||||||
|
|
||||||
|
# Package control specific files Package
|
||||||
|
Control.last-run
|
||||||
|
Control.ca-list
|
||||||
|
Control.ca-bundle
|
||||||
|
Control.system-ca-bundle
|
||||||
|
GitHub.sublime-settings
|
||||||
|
|
||||||
|
# Visual Studio Code #
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 dyvenia
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
141
README.md
141
README.md
|
@ -1,2 +1,139 @@
|
||||||
# cotton
|
# Cotton
|
||||||
Bringing component based design to Django templates
|
|
||||||
|
Bringing component-based design to Django templates.
|
||||||
|
|
||||||
|
<a href="https://www.django-cotton.com" target="_blank">Document site</a>
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Cotton enhances Django templates by allowing component-based design, making UI composition more efficient and reusable. It integrates seamlessly with Tailwind CSS and retains full compatibility with native Django template features.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
- **Rapid UI Composition:** Efficiently compose and reuse UI components.
|
||||||
|
- **Tailwind CSS Harmony:** Integrates with Tailwind's utility-first approach.
|
||||||
|
- **Interoperable with Django:** Enhances Django templates without replacing them.
|
||||||
|
- **Semantic Syntax:** HTML-like syntax for better code editor support.
|
||||||
|
- **Minimal Overhead:** Compiles to native Django components with automatic caching.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
To install Cotton, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install django-cotton
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update your `settings.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
...
|
||||||
|
'django_cotton',
|
||||||
|
]
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': ['your_project/templates'],
|
||||||
|
'APP_DIRS': False,
|
||||||
|
'OPTIONS': {
|
||||||
|
'loaders': [
|
||||||
|
'django_cotton.template.loaders.CottonLoader',
|
||||||
|
# continue with default loaders:
|
||||||
|
# "django.template.loaders.filesystem.Loader",
|
||||||
|
# "django.template.loaders.app_directories.Loader",
|
||||||
|
],
|
||||||
|
'builtins': [
|
||||||
|
'django_cotton.templatetags.cotton',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quickstart
|
||||||
|
Create a new directory in your templates directory called `cotton`. Inside this directory, create a new file called `card.cotton.html` with the following content:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="bg-white shadow rounded border p-4">
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
<p>{{ slot }}</p>
|
||||||
|
<button href="{% url url %}">Read more</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a view with a template. Views that contain Cotton components must also use the `.cotton.html` extension:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# views.py
|
||||||
|
def dashboard_view(request):
|
||||||
|
return render(request, "dashboard.cotton.html")
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- templates/dashboard.cotton.html -->
|
||||||
|
<c-card title="Trees" url="trees">
|
||||||
|
We have the best trees
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="Spades" url="spades">
|
||||||
|
The best spades in the land
|
||||||
|
</c-card>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage Basics
|
||||||
|
- **Template Extensions:** View templates including Cotton components should use the `.cotton.html` extension.
|
||||||
|
- **Component Placement:** Components should be placed in the `templates/cotton` folder.
|
||||||
|
- **Naming Conventions:**
|
||||||
|
- Component filenames use snake_case: `my_component.cotton.html`
|
||||||
|
- Components are called using kebab-case: `<c-my-component />`
|
||||||
|
|
||||||
|
### Example
|
||||||
|
A minimal example using Cotton components:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- my_component.cotton.html -->
|
||||||
|
{{ slot }}
|
||||||
|
|
||||||
|
<!-- my_view.cotton.html -->
|
||||||
|
<c-my-component>
|
||||||
|
<p>Some content</p>
|
||||||
|
</c-my-component>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attributes and Slots
|
||||||
|
Components can accept attributes and named slots for flexible content and behavior customization:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- weather.cotton.html -->
|
||||||
|
<p>It's {{ temperature }}<sup>{{ unit }}</sup> and the condition is {{ condition }}.</p>
|
||||||
|
|
||||||
|
<!-- view.cotton.html -->
|
||||||
|
<c-weather temperature="23" unit="c" condition="windy"></c-weather>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passing Variables
|
||||||
|
To pass a variable from the parent's context, prepend the attribute with a `:`.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- view.cotton.html -->
|
||||||
|
<c-weather :unit="unit"></c-weather>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Named Slots
|
||||||
|
```html
|
||||||
|
<!-- weather_card.cotton.html -->
|
||||||
|
<div class="flex ...">
|
||||||
|
<h2>{{ day }}:</h2> {{ icon }} {{ label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- view.cotton.html -->
|
||||||
|
<c-weather-card day="Tuesday">
|
||||||
|
<c-slot name="icon">
|
||||||
|
<svg>...</svg>
|
||||||
|
</c-slot>
|
||||||
|
<c-slot name="label">
|
||||||
|
<h2 class="text-yellow-500">Sunny</h2>
|
||||||
|
</c-slot>
|
||||||
|
</c-weather-card>
|
||||||
|
```
|
29
dev/docker/Dockerfile
Normal file
29
dev/docker/Dockerfile
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
ARG PLATFORM=linux/amd64
|
||||||
|
|
||||||
|
# Use an official Python runtime as a base image
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Keep logs unbuffered
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
# Set the working directory in the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
RUN pip install --upgrade pip \
|
||||||
|
&& pip install poetry
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
chromium \
|
||||||
|
chromium-driver \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy only dependencies definition to the docker image
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# Install project dependencies
|
||||||
|
RUN poetry config virtualenvs.create false \
|
||||||
|
&& poetry install
|
||||||
|
|
||||||
|
CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]
|
16
dev/docker/bin/build.sh
Executable file
16
dev/docker/bin/build.sh
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# if arg exists 'mac', then build for mac
|
||||||
|
if [ "$1" = "mac" ]; then
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
else
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
|
||||||
|
docker build --no-cache --build-arg PLATFORM=$PLATFORM -f ../Dockerfile ../../example_project -t cotton-test-app
|
4
dev/docker/bin/manage.sh
Executable file
4
dev/docker/bin/manage.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Start a new login shell to preserve history and execute the Docker command
|
||||||
|
bash -l -c "docker exec -it cotton-dev-app python manage.py $*"
|
9
dev/docker/bin/run-dev.sh
Executable file
9
dev/docker/bin/run-dev.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
|
||||||
|
docker compose -f ../docker-compose.yaml up "$@"
|
9
dev/docker/bin/shell.sh
Executable file
9
dev/docker/bin/shell.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR/..
|
||||||
|
|
||||||
|
docker exec -it cotton-dev-app python manage.py shell
|
9
dev/docker/bin/stop.sh
Executable file
9
dev/docker/bin/stop.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR/..
|
||||||
|
|
||||||
|
docker compose -f etc/docker/docker-compose.yaml -f etc/docker/docker-compose.dev.yaml down $*
|
3
dev/docker/bin/terminal.sh
Executable file
3
dev/docker/bin/terminal.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
docker exec -it cotton-dev-app bash
|
3
dev/docker/bin/test.sh
Executable file
3
dev/docker/bin/test.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
docker exec -t cotton-dev-app python manage.py test $*
|
16
dev/docker/docker-compose.yaml
Normal file
16
dev/docker/docker-compose.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
container_name: cotton-dev-app
|
||||||
|
restart: always
|
||||||
|
image: cotton-test-app
|
||||||
|
working_dir: /app
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
environment:
|
||||||
|
- DEBUG=True
|
||||||
|
volumes:
|
||||||
|
- ../example_project:/app
|
||||||
|
- ../../django_cotton:/app/django_cotton
|
||||||
|
ports:
|
||||||
|
- 8001:8000
|
0
dev/example_project/example_project/__init__.py
Normal file
0
dev/example_project/example_project/__init__.py
Normal file
16
dev/example_project/example_project/asgi.py
Normal file
16
dev/example_project/example_project/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for django_cotton project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
136
dev/example_project/example_project/settings.py
Normal file
136
dev/example_project/example_project/settings.py
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
"""
|
||||||
|
Django settings for django_cotton project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.2.8.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# SETTINGS_PATH = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = "django-insecure-%)7a&zw=le4uey^36*z*9^4#*iii65t)nyt$36mxq70@=(z6^n"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["0.0.0.0"]
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django_cotton",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "django_cotton.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": ["example_project/templates", "django_cotton/templates"],
|
||||||
|
"APP_DIRS": False,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
"loaders": [
|
||||||
|
"django_cotton.cotton_loader.Loader",
|
||||||
|
"django.template.loaders.filesystem.Loader",
|
||||||
|
"django.template.loaders.app_directories.Loader",
|
||||||
|
],
|
||||||
|
"builtins": [
|
||||||
|
"django.templatetags.static",
|
||||||
|
"django_cotton.templatetags.cotton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "django_cotton.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
COTTON_TEMPLATE_CACHING_ENABLED = False
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-parent>
|
||||||
|
<c-child>d</c-child>
|
||||||
|
</c-parent>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<c-benchmarks.partials.main>
|
||||||
|
I'm default
|
||||||
|
<c-slot name="top">
|
||||||
|
I'm top
|
||||||
|
</c-slot>
|
||||||
|
<c-slot name="bottom">
|
||||||
|
I'm bottom
|
||||||
|
</c-slot>
|
||||||
|
</c-benchmarks.partials.main>
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% cotton_component cotton/benchmarks/partials/main.cotton.html %}
|
||||||
|
I'm default
|
||||||
|
{% cotton_slot top %}
|
||||||
|
I'm top
|
||||||
|
{% end_cotton_slot %}
|
||||||
|
{% cotton_slot bottom %}
|
||||||
|
I'm bottom
|
||||||
|
{% end_cotton_slot %}
|
||||||
|
{% end_cotton_component %}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "cotton/benchmarks/partials/native_main.html" %}
|
||||||
|
|
||||||
|
{% block top %}
|
||||||
|
I'm top
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
I'm default
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bottom %}
|
||||||
|
I'm bottom
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{{ top }}
|
||||||
|
|
||||||
|
{{ slot }}
|
||||||
|
|
||||||
|
{{ bottom }}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% block top %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
{% block bottom %}{% endblock %}
|
|
@ -0,0 +1 @@
|
||||||
|
<div class="i-am-child"></div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
{{ name }}
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="i-am-parent">
|
||||||
|
{{slot}}
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<c-props prop1="sds" prop_with_default="1" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ testy }}
|
||||||
|
<p>prop1: '{{ prop1 }}'</p>
|
||||||
|
<p>attr1: '{{ attr1 }}'</p>
|
||||||
|
<p>empty_prop: '{{ empty_prop }}'</p>
|
||||||
|
<p>prop_with_default: '{{ prop_with_default }}'</p>
|
||||||
|
<p>slot: '{{ slot }}'</p>
|
||||||
|
<p>named_slot: '{{ named_slot }}'</p>
|
||||||
|
<p>attrs: '{{ attrs }}'</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-parent>
|
||||||
|
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
|
||||||
|
</c-parent>
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% for item in items %}
|
||||||
|
<c-named-slot-component>
|
||||||
|
<c-slot name="name">
|
||||||
|
item name: {{ item.name }}
|
||||||
|
</c-slot>
|
||||||
|
</c-named-slot-component>
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
<c-parent></c-parent>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-props-test-component prop1="im a prop" attr1="im an attr">
|
||||||
|
default slot
|
||||||
|
</c-props-test-component>
|
|
@ -0,0 +1,4 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
|
<c-parent/>
|
22
dev/example_project/manage.py
Executable file
22
dev/example_project/manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
24
dev/example_project/pyproject.toml
Normal file
24
dev/example_project/pyproject.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[project]
|
||||||
|
name = "cotton-dev-app"
|
||||||
|
requires-python = ">=3.8, <4"
|
||||||
|
dependencies = [
|
||||||
|
"django~=4.2.6",
|
||||||
|
"beautifulsoup4~=4.12.2",
|
||||||
|
"selenium~=4.13.0",
|
||||||
|
"chromedriver-py~=117.0.5938.92",
|
||||||
|
"webdriver-manager~=4.0.1"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
name = "cotton-dev-app"
|
||||||
|
version = "0.1"
|
||||||
|
description = "Development and test app for the django package."
|
||||||
|
authors = ["Will Abbott <wabbott@dyvenia.com>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.8"
|
||||||
|
Django = "^4.2"
|
||||||
|
beautifulsoup4 = "~4.12.2"
|
||||||
|
selenium = "~4.13.0"
|
||||||
|
chromedriver-py = "~117.0.5938.92"
|
||||||
|
webdriver-manager = "~4.0.1"
|
71
dev/example_project/render_load_test.py
Normal file
71
dev/example_project/render_load_test.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
from django.conf import settings
|
||||||
|
import time
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
import django
|
||||||
|
|
||||||
|
# Configure Django settings
|
||||||
|
settings.configure(
|
||||||
|
INSTALLED_APPS=[
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django_cotton",
|
||||||
|
],
|
||||||
|
TEMPLATES=[
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": ["example_project/templates"],
|
||||||
|
"APP_DIRS": False,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
"loaders": [
|
||||||
|
"django_cotton.cotton_loader.Loader",
|
||||||
|
"django.template.loaders.filesystem.Loader",
|
||||||
|
"django.template.loaders.app_directories.Loader",
|
||||||
|
],
|
||||||
|
"builtins": [
|
||||||
|
"django.templatetags.static",
|
||||||
|
"django_cotton.templatetags.cotton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
|
||||||
|
def benchmark_template_rendering(template_name, iterations=1000):
|
||||||
|
start_time = time.time()
|
||||||
|
for _ in range(iterations):
|
||||||
|
render_to_string(template_name)
|
||||||
|
end_time = time.time()
|
||||||
|
return end_time - start_time, render_to_string(template_name)
|
||||||
|
|
||||||
|
|
||||||
|
# Benchmarking each template
|
||||||
|
time_native_extends, output_native_extends = benchmark_template_rendering(
|
||||||
|
"cotton/benchmarks/native_extends.html"
|
||||||
|
)
|
||||||
|
# time_native_include, output_native_include = benchmark_template_rendering('cotton/benchmarks/native_include.html')
|
||||||
|
time_compiled_cotton, output_compiled_cotton = benchmark_template_rendering(
|
||||||
|
"cotton/benchmarks/cotton_compiled.html"
|
||||||
|
)
|
||||||
|
time_cotton, output_cotton = benchmark_template_rendering(
|
||||||
|
"cotton/benchmarks/cotton.cotton.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
print(f"Native Django Template using extend: {time_native_extends} seconds")
|
||||||
|
# print(f"Native Django Template using include: {time_native_include} seconds")
|
||||||
|
print(f"Compiled Cotton Template: {time_compiled_cotton} seconds")
|
||||||
|
print(f"Cotton Template: {time_cotton} seconds")
|
1
django_cotton/__init__.py
Normal file
1
django_cotton/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
_with_prop_prefix = "cotton_with_prop_"
|
291
django_cotton/cotton_loader.py
Executable file
291
django_cotton/cotton_loader.py
Executable file
|
@ -0,0 +1,291 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import hashlib
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from django.template.loaders.base import Loader as BaseLoader
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
|
from django.template import TemplateDoesNotExist
|
||||||
|
from django.utils._os import safe_join
|
||||||
|
from django.template import Template
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.template import Origin
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||||
|
|
||||||
|
|
||||||
|
class Loader(BaseLoader):
|
||||||
|
is_usable = True
|
||||||
|
|
||||||
|
def __init__(self, engine, dirs=None):
|
||||||
|
super().__init__(engine)
|
||||||
|
self.cache_handler = CottonTemplateCacheHandler()
|
||||||
|
self.dirs = dirs
|
||||||
|
self.django_syntax_placeholders = []
|
||||||
|
|
||||||
|
def get_template_from_string(self, template_string):
|
||||||
|
"""Create and return a Template object from a string. Used primarily for testing."""
|
||||||
|
return Template(template_string, engine=self.engine)
|
||||||
|
|
||||||
|
def get_contents(self, origin):
|
||||||
|
# check if file exists, whilst getting the mtime for cache key
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(origin.name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise TemplateDoesNotExist(origin)
|
||||||
|
|
||||||
|
# check and return cached template
|
||||||
|
cache_key = self.cache_handler.get_cache_key(origin.template_name, mtime)
|
||||||
|
cached_content = self.cache_handler.get_cached_template(cache_key)
|
||||||
|
if cached_content is not None:
|
||||||
|
return cached_content
|
||||||
|
|
||||||
|
# If not cached, process the template
|
||||||
|
template_string = self._get_template_string(origin.name)
|
||||||
|
|
||||||
|
# We need to provide a key to the current view or component (in this case, view) so that we can namespace
|
||||||
|
# slot data, preventing bleeding and ensure component's clear only data in the context applicable to itself
|
||||||
|
# in this case, we're top level, likely in a view so we use the view template name as the key
|
||||||
|
component_key = (
|
||||||
|
origin.template_name.lstrip("cotton/")
|
||||||
|
.rstrip(".cotton.html")
|
||||||
|
.replace("/", ".")
|
||||||
|
)
|
||||||
|
|
||||||
|
compiled_template = self._compile_template_from_string(
|
||||||
|
template_string, component_key
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cache the processed template
|
||||||
|
self.cache_handler.cache_template(cache_key, compiled_template)
|
||||||
|
|
||||||
|
return compiled_template
|
||||||
|
|
||||||
|
def _replace_syntax_with_placeholders(self, content):
|
||||||
|
"""# replace {% ... %} and {{ ... }} with placeholders so they dont get touched
|
||||||
|
or encoded by bs4. Store them to later switch them back in after transformation.
|
||||||
|
"""
|
||||||
|
self.django_syntax_placeholders = []
|
||||||
|
|
||||||
|
# First handle cotton_verbatim blocks, this is designed to preserve and display cotton syntax,
|
||||||
|
# akin to the verbatim tag in Django.
|
||||||
|
def replace_cotton_verbatim(match):
|
||||||
|
inner_content = match.group(
|
||||||
|
1
|
||||||
|
) # Get the inner content without the cotton_verbatim tags
|
||||||
|
self.django_syntax_placeholders.append(inner_content)
|
||||||
|
return f"__django_syntax__{len(self.django_syntax_placeholders)}__"
|
||||||
|
|
||||||
|
# Replace cotton_verbatim blocks, capturing inner content
|
||||||
|
content = re.sub(
|
||||||
|
r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}",
|
||||||
|
replace_cotton_verbatim,
|
||||||
|
content,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r"\{%.*?%\}",
|
||||||
|
lambda x: self.django_syntax_placeholders.append(x.group(0))
|
||||||
|
or f"__django_syntax__{len(self.django_syntax_placeholders)}__",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
content = re.sub(
|
||||||
|
r"\{\{.*?\}\}",
|
||||||
|
lambda x: self.django_syntax_placeholders.append(x.group(0))
|
||||||
|
or f"__django_syntax__{len(self.django_syntax_placeholders)}__",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _replace_placeholders_with_syntax(self, content):
|
||||||
|
"""After modifying the content, replace the placeholders with the django template tags and variables."""
|
||||||
|
for i, placeholder in enumerate(self.django_syntax_placeholders, 1):
|
||||||
|
content = content.replace(f"__django_syntax__{i}__", placeholder)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _get_template_string(self, template_name):
|
||||||
|
try:
|
||||||
|
with open(template_name, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise TemplateDoesNotExist(template_name)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _compile_template_from_string(self, content, component_key):
|
||||||
|
content = self._replace_syntax_with_placeholders(content)
|
||||||
|
content = self._compile_cotton_to_django(content, component_key)
|
||||||
|
content = self._replace_placeholders_with_syntax(content)
|
||||||
|
content = self._revert_bs4_attribute_empty_attribute_fixing(content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _revert_bs4_attribute_empty_attribute_fixing(self, contents):
|
||||||
|
"""Django's template parser adds ="" to empty attribute-like parts in any html-like node, i.e. <div {{ something }}> gets
|
||||||
|
compiled to <div {{ something }}=""> Then if 'something' is holding attributes sets, the last attribute value is
|
||||||
|
not quoted. i.e. model=test not model="test"."""
|
||||||
|
cleaned_content = re.sub('}}=""', "}}", contents)
|
||||||
|
return cleaned_content
|
||||||
|
|
||||||
|
def get_dirs(self):
|
||||||
|
return self.dirs if self.dirs is not None else self.engine.dirs
|
||||||
|
|
||||||
|
def get_template_sources(self, template_name):
|
||||||
|
"""Return an Origin object pointing to an absolute path in each directory
|
||||||
|
in template_dirs. For security reasons, if a path doesn't lie inside
|
||||||
|
one of the template_dirs it is excluded from the result set."""
|
||||||
|
if template_name.endswith(".cotton.html"):
|
||||||
|
for template_dir in self.get_dirs():
|
||||||
|
try:
|
||||||
|
name = safe_join(template_dir, template_name)
|
||||||
|
except SuspiciousFileOperation:
|
||||||
|
# The joined path was located outside of this template_dir
|
||||||
|
# (it might be inside another one, so this isn't fatal).
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield Origin(
|
||||||
|
name=name,
|
||||||
|
template_name=template_name,
|
||||||
|
loader=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compile_cotton_to_django(self, html_content, component_key):
|
||||||
|
"""Convert cotton <c-* syntax to {%."""
|
||||||
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
|
|
||||||
|
soup = self._wrap_with_cotton_props_frame(soup)
|
||||||
|
self._transform_components(soup, component_key)
|
||||||
|
|
||||||
|
return str(soup)
|
||||||
|
|
||||||
|
def _transform_prop_tags(self, soup):
|
||||||
|
c_props = soup.find_all("c-props")
|
||||||
|
|
||||||
|
for tag in c_props:
|
||||||
|
# Build the cotton_props tag string
|
||||||
|
props_list = []
|
||||||
|
for prop, value in tag.attrs.items():
|
||||||
|
if value is None:
|
||||||
|
props_list.append(prop)
|
||||||
|
else:
|
||||||
|
props_list.append(f'{prop}="{value}"')
|
||||||
|
|
||||||
|
cotton_props_str = "{% cotton_props " + " ".join(props_list) + " %}"
|
||||||
|
|
||||||
|
# Replace the <c-props> tag with the cotton_props string.
|
||||||
|
tag.replace_with(cotton_props_str)
|
||||||
|
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def _wrap_with_cotton_props_frame(self, soup):
|
||||||
|
"""Wrap content with {% cotton_props_frame %} to be able to govern props and attributes. In order to recognise
|
||||||
|
props defined in a component and also have them available in context, we wrap the entire contents in another
|
||||||
|
component: cotton_props_frame."""
|
||||||
|
props_with_defaults = []
|
||||||
|
c_props = soup.find("c-props")
|
||||||
|
|
||||||
|
# parse c-props tag to extract properties and defaults
|
||||||
|
if c_props:
|
||||||
|
props_with_defaults = []
|
||||||
|
for prop, value in c_props.attrs.items():
|
||||||
|
if value is None:
|
||||||
|
props_with_defaults.append(f"{prop}={prop}")
|
||||||
|
else:
|
||||||
|
# Assuming value is already a string that represents the default value
|
||||||
|
props_with_defaults.append(f'{prop}={prop}|default:"{value}"')
|
||||||
|
|
||||||
|
c_props.decompose()
|
||||||
|
|
||||||
|
# Construct the {% with %} opening tag
|
||||||
|
opening = "{% cotton_props_frame " + " ".join(props_with_defaults) + " %}"
|
||||||
|
closing = "{% endcotton_props_frame %}"
|
||||||
|
|
||||||
|
# Convert the remaining soup back to a string and wrap it within {% with %} block
|
||||||
|
wrapped_content = opening + str(soup).strip() + closing
|
||||||
|
|
||||||
|
# Since we can't replace the soup object itself, we create new soup instead
|
||||||
|
new_soup = BeautifulSoup(wrapped_content, "html.parser")
|
||||||
|
|
||||||
|
return new_soup
|
||||||
|
|
||||||
|
def _transform_named_slot(self, slot_tag, component_key):
|
||||||
|
"""Replace <c-slot> tags with the {% cotton_slot %} template tag"""
|
||||||
|
# for c_slot in soup.find_all("c-slot"):
|
||||||
|
slot_name = slot_tag.get("name", "").strip()
|
||||||
|
inner_html = "".join(str(content) for content in slot_tag.contents)
|
||||||
|
|
||||||
|
# Check and process any components in the slot content
|
||||||
|
|
||||||
|
slot_soup = BeautifulSoup(inner_html, "html.parser")
|
||||||
|
self._transform_components(slot_soup, component_key)
|
||||||
|
|
||||||
|
cotton_slot_tag = f"{{% cotton_slot {slot_name} {component_key} %}}{str(slot_soup)}{{% end_cotton_slot %}}"
|
||||||
|
|
||||||
|
slot_tag.replace_with(BeautifulSoup(cotton_slot_tag, "html.parser"))
|
||||||
|
|
||||||
|
def _transform_components(self, soup, component_key):
|
||||||
|
"""Replace <c-[component path]> tags with the {% cotton_component %} template tag"""
|
||||||
|
for tag in soup.find_all(re.compile("^c-"), recursive=True):
|
||||||
|
if tag.name == "c-slot":
|
||||||
|
self._transform_named_slot(tag, component_key)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
component_name = tag.name[2:]
|
||||||
|
|
||||||
|
# Convert dot notation to path structure and replace hyphens with underscores
|
||||||
|
path = component_name.replace(".", "/").replace("-", "_")
|
||||||
|
|
||||||
|
# Construct the opening tag
|
||||||
|
opening_tag = f"{{% cotton_component {'cotton/{}.cotton.html'.format(path)} {component_name} "
|
||||||
|
for attr, value in tag.attrs.items():
|
||||||
|
if attr == "class":
|
||||||
|
value = " ".join(value)
|
||||||
|
opening_tag += ' {}="{}"'.format(attr, value)
|
||||||
|
opening_tag += " %}"
|
||||||
|
|
||||||
|
# Construct the closing tag
|
||||||
|
closing_tag = "{% end_cotton_component %}"
|
||||||
|
|
||||||
|
if tag.contents:
|
||||||
|
tag_soup = BeautifulSoup(tag.decode_contents(), "html.parser")
|
||||||
|
self._transform_components(tag_soup, component_name)
|
||||||
|
|
||||||
|
# Create new content with opening tag, tag content, and closing tag
|
||||||
|
new_content = opening_tag + str(tag_soup) + closing_tag
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Create new content with opening tag and closing tag
|
||||||
|
new_content = opening_tag + closing_tag
|
||||||
|
|
||||||
|
# Replace the original tag with the new content
|
||||||
|
new_soup = BeautifulSoup(new_content, "html.parser")
|
||||||
|
tag.replace_with(new_soup)
|
||||||
|
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
class CottonTemplateCacheHandler:
|
||||||
|
"""Handles caching of cotton templates so the html parsing is only done on first load of each view or component."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = getattr(settings, "TEMPLATE_CACHING_ENABLED", True)
|
||||||
|
|
||||||
|
def get_cache_key(self, template_name, mtime):
|
||||||
|
template_hash = hashlib.sha256(template_name.encode()).hexdigest()
|
||||||
|
return f"cotton_cache_{template_hash}_{mtime}"
|
||||||
|
|
||||||
|
def get_cached_template(self, cache_key):
|
||||||
|
if not self.enabled:
|
||||||
|
return None
|
||||||
|
return cache.get(cache_key)
|
||||||
|
|
||||||
|
def cache_template(self, cache_key, content, timeout=None):
|
||||||
|
if self.enabled:
|
||||||
|
cache.set(cache_key, content, timeout=timeout)
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-merges-attributes class="extra-class" silica:model="test" another="test">
|
||||||
|
ss
|
||||||
|
</c-merges-attributes>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-receives-attributes attribute_1="hello" and-another="woo1" thirdForLuck="yes">
|
||||||
|
ss
|
||||||
|
</c-receives-attributes>
|
3
django_cotton/templates/child_test.cotton.html
Normal file
3
django_cotton/templates/child_test.cotton.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<c-parent>
|
||||||
|
<c-child>d</c-child>
|
||||||
|
</c-parent>
|
1
django_cotton/templates/cotton/child.cotton.html
Normal file
1
django_cotton/templates/cotton/child.cotton.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div class="i-am-child"></div>
|
11
django_cotton/templates/cotton/container.cotton.html
Normal file
11
django_cotton/templates/cotton/container.cotton.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div>
|
||||||
|
Header:
|
||||||
|
{{ header }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Content:
|
||||||
|
{{ slot }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div {{ attrs_dict|merge:'class:form-group another-class-with:colon' }}>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
{{ name }}
|
||||||
|
</div>
|
3
django_cotton/templates/cotton/parent.cotton.html
Normal file
3
django_cotton/templates/cotton/parent.cotton.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="i-am-parent">
|
||||||
|
{{slot}}
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div {{ attrs }}>
|
||||||
|
|
||||||
|
</div>
|
13
django_cotton/templates/cotton/test_component.cotton.html
Normal file
13
django_cotton/templates/cotton/test_component.cotton.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<c-props prop1 default_prop="default prop" />
|
||||||
|
|
||||||
|
<p>slot: '{{ slot }}'</p>
|
||||||
|
|
||||||
|
<p>attr1: '{{ attr1 }}'</p>
|
||||||
|
<p>attr2: '{{ attr2 }}'</p>
|
||||||
|
|
||||||
|
<p>prop1: '{{ prop1 }}'</p>
|
||||||
|
<p>default_prop: '{{ default_prop }}'</p>
|
||||||
|
|
||||||
|
<p>named_slot: '{{ named_slot }}'</p>
|
||||||
|
|
||||||
|
<p>attrs: '{{ attrs }}'</p>
|
|
@ -0,0 +1 @@
|
||||||
|
<div class="{% if 1 < 2 %} some-class {% endif %}">Hello, World!</div>
|
3
django_cotton/templates/form_test.cotton.html
Normal file
3
django_cotton/templates/form_test.cotton.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<c-parent>
|
||||||
|
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
|
||||||
|
</c-parent>
|
7
django_cotton/templates/named_slot_in_loop.cotton.html
Normal file
7
django_cotton/templates/named_slot_in_loop.cotton.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% for item in items %}
|
||||||
|
<c-named-slot-component>
|
||||||
|
<c-slot name="name">
|
||||||
|
item name: {{ item.name }}
|
||||||
|
</c-slot>
|
||||||
|
</c-named-slot-component>
|
||||||
|
{% endfor %}
|
1
django_cotton/templates/parent_test.cotton.html
Normal file
1
django_cotton/templates/parent_test.cotton.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<c-parent></c-parent>
|
4
django_cotton/templates/self_closing_test.cotton.html
Normal file
4
django_cotton/templates/self_closing_test.cotton.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
|
<c-parent/>
|
5
django_cotton/templates/string_with_spaces.cotton.html
Normal file
5
django_cotton/templates/string_with_spaces.cotton.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<c-test-component prop1="string with space" attr1="I have spaces">
|
||||||
|
<c-slot name="named_slot">
|
||||||
|
named_slot with spaces
|
||||||
|
</c-slot>
|
||||||
|
</c-test-component>
|
|
@ -0,0 +1,2 @@
|
||||||
|
<c-test-component attr1="variable" :attr2="variable">
|
||||||
|
</c-test-component>
|
0
django_cotton/templatetags/__init__.py
Normal file
0
django_cotton/templatetags/__init__.py
Normal file
68
django_cotton/templatetags/_component.py
Normal file
68
django_cotton/templatetags/_component.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from django import template
|
||||||
|
from django.template import Node
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
|
||||||
|
def cotton_component(parser, token):
|
||||||
|
bits = token.split_contents()
|
||||||
|
tag_name = bits[0]
|
||||||
|
template_path = bits[1]
|
||||||
|
component_key = bits[2]
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
for bit in bits[3:]:
|
||||||
|
key, value = bit.split("=")
|
||||||
|
if key.startswith(":"): # Detect variables
|
||||||
|
key = key[1:] # Remove ':' prefix
|
||||||
|
value = value.strip("'\"") # Remove quotes
|
||||||
|
kwargs[key] = template.Variable(value) # Treat as a variable
|
||||||
|
else:
|
||||||
|
kwargs[key] = value.strip("'\"") # Treat as a literal string
|
||||||
|
|
||||||
|
nodelist = parser.parse(("end_cotton_component",))
|
||||||
|
parser.delete_first_token()
|
||||||
|
|
||||||
|
return CottonComponentNode(nodelist, template_path, component_key, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CottonComponentNode(Node):
|
||||||
|
def __init__(self, nodelist, template_path, component_key, kwargs):
|
||||||
|
self.nodelist = nodelist
|
||||||
|
self.template_path = template_path
|
||||||
|
self.component_key = component_key
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
local_context = context.flatten()
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
for key, value in self.kwargs.items():
|
||||||
|
if isinstance(value, template.Variable): # Resolve variables
|
||||||
|
try:
|
||||||
|
resolved_value = value.resolve(context)
|
||||||
|
attrs[key] = resolved_value
|
||||||
|
except template.VariableDoesNotExist:
|
||||||
|
pass # Handle variable not found, if necessary
|
||||||
|
else:
|
||||||
|
attrs[key] = value # Use literal string
|
||||||
|
|
||||||
|
# Add the remainder as the default slot
|
||||||
|
rendered = self.nodelist.render(context)
|
||||||
|
local_context.update({"slot": rendered})
|
||||||
|
|
||||||
|
slots = context.get("cotton_slots", {})
|
||||||
|
component_slots = slots.get(self.component_key, {})
|
||||||
|
|
||||||
|
local_context.update(component_slots)
|
||||||
|
local_context.update(attrs)
|
||||||
|
local_context.update({"attrs_dict": attrs})
|
||||||
|
|
||||||
|
rendered = render_to_string(self.template_path, local_context)
|
||||||
|
|
||||||
|
# Now reset the component's slots in context to prevent bleeding
|
||||||
|
if self.component_key in slots:
|
||||||
|
slots[self.component_key] = {}
|
||||||
|
|
||||||
|
context.update({"cotton_slots": slots})
|
||||||
|
|
||||||
|
return rendered
|
43
django_cotton/templatetags/_props.py
Normal file
43
django_cotton/templatetags/_props.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
def cotton_props(parser, token):
|
||||||
|
# Split the token to get variable assignments
|
||||||
|
parts = token.split_contents()
|
||||||
|
cotton_props = {}
|
||||||
|
for part in parts[1:]:
|
||||||
|
key, value = part.split("=")
|
||||||
|
cotton_props[key] = value
|
||||||
|
|
||||||
|
return CottonPropNode(cotton_props)
|
||||||
|
|
||||||
|
|
||||||
|
class CottonPropNode(template.Node):
|
||||||
|
def __init__(self, cotton_props):
|
||||||
|
self.cotton_props = cotton_props
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
resolved_props = {}
|
||||||
|
|
||||||
|
# if the same var is already set in context, it's being passed explicitly to override the cotton_var
|
||||||
|
# if not, then we resolve it from the context
|
||||||
|
for key, value in self.cotton_props.items():
|
||||||
|
# if key in context:
|
||||||
|
# resolved_props[key] = context[key]
|
||||||
|
# continue
|
||||||
|
try:
|
||||||
|
resolved_props[key] = template.Variable(value).resolve(context)
|
||||||
|
except (TypeError, template.VariableDoesNotExist):
|
||||||
|
resolved_props[key] = value
|
||||||
|
|
||||||
|
cotton_props = {"cotton_props": resolved_props}
|
||||||
|
|
||||||
|
# Update the global context directly
|
||||||
|
context.update(resolved_props)
|
||||||
|
context.update(cotton_props)
|
||||||
|
context.update({"cotton_props": resolved_props})
|
||||||
|
context["cotton_props"].update(resolved_props)
|
||||||
|
|
||||||
|
return ""
|
64
django_cotton/templatetags/_props_frame.py
Normal file
64
django_cotton/templatetags/_props_frame.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
from django import template
|
||||||
|
from django.template.base import token_kwargs
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
def cotton_props_frame(parser, token):
|
||||||
|
"""The job of the props frame is to filter component kwargs (attributes) against declared props. It has to be
|
||||||
|
second component because we desire to declare props (<c-props />) inside the component template and therefore the
|
||||||
|
component can not manipulate its own context from it's own template, instead we declare the props frame
|
||||||
|
directly inside component"""
|
||||||
|
bits = token.split_contents()[1:] # Skip the tag name
|
||||||
|
# Parse token kwargs while maintaining token order
|
||||||
|
tag_kwargs = token_kwargs(bits, parser)
|
||||||
|
|
||||||
|
nodelist = parser.parse(("endcotton_props_frame",))
|
||||||
|
parser.delete_first_token()
|
||||||
|
return CottonPropsFrameNode(nodelist, tag_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CottonPropsFrameNode(template.Node):
|
||||||
|
def __init__(self, nodelist, kwargs):
|
||||||
|
self.nodelist = nodelist
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
# Assume 'attrs' are passed from the parent and are available in the context
|
||||||
|
parent_attrs = context.get("attrs_dict", {})
|
||||||
|
|
||||||
|
# Initialize props based on the frame's kwargs and parent attrs
|
||||||
|
props = {}
|
||||||
|
for key, value in self.kwargs.items():
|
||||||
|
# Attempt to resolve each kwarg value (which may include template variables)
|
||||||
|
resolved_value = value.resolve(context)
|
||||||
|
# Check if the prop exists in parent attrs; if so, use it, otherwise use the resolved default
|
||||||
|
if key in parent_attrs:
|
||||||
|
props[key] = parent_attrs[key]
|
||||||
|
else:
|
||||||
|
props[key] = resolved_value
|
||||||
|
|
||||||
|
# Overwrite 'attrs' in the local context by excluding keys that are identified as props
|
||||||
|
attrs_without_props = {k: v for k, v in parent_attrs.items() if k not in props}
|
||||||
|
context["attrs_dict"] = attrs_without_props
|
||||||
|
|
||||||
|
# Provide all of the attrs as a string to pass to the component
|
||||||
|
def ensure_quoted(value):
|
||||||
|
if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return f'"{value}"'
|
||||||
|
|
||||||
|
attrs = " ".join(
|
||||||
|
[
|
||||||
|
f"{key}={ensure_quoted(value)}"
|
||||||
|
for key, value in attrs_without_props.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
context.update({"attrs": mark_safe(attrs)})
|
||||||
|
context.update(attrs_without_props)
|
||||||
|
context.update(props)
|
||||||
|
|
||||||
|
return self.nodelist.render(context)
|
48
django_cotton/templatetags/_slot.py
Normal file
48
django_cotton/templatetags/_slot.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from django import template
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
def cotton_slot(parser, token):
|
||||||
|
try:
|
||||||
|
tag_name, slot_name, component_key = token.split_contents()
|
||||||
|
except ValueError:
|
||||||
|
raise template.TemplateSyntaxError("incomplete c-slot %r" % token.contents)
|
||||||
|
|
||||||
|
nodelist = parser.parse(("end_cotton_slot",))
|
||||||
|
parser.delete_first_token()
|
||||||
|
return CottonSlotNode(slot_name, nodelist, component_key)
|
||||||
|
|
||||||
|
|
||||||
|
class CottonSlotNode(template.Node):
|
||||||
|
def __init__(self, slot_name, nodelist, component_key):
|
||||||
|
self.slot_name = slot_name
|
||||||
|
self.nodelist = nodelist
|
||||||
|
self.component_key = component_key
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
# Add the rendered content to the context.
|
||||||
|
if "cotton_slots" not in context:
|
||||||
|
context.update({"cotton_slots": {}})
|
||||||
|
|
||||||
|
# context["cotton_slots"][self.slot_name] = mark_safe(output)
|
||||||
|
|
||||||
|
output = self.nodelist.render(context)
|
||||||
|
|
||||||
|
# with context.push():
|
||||||
|
# Temporarily store the slot's content in the new layer
|
||||||
|
# if "cotton_slots" not in context:
|
||||||
|
# context["cotton_slots"] = {}
|
||||||
|
|
||||||
|
if self.component_key not in context["cotton_slots"]:
|
||||||
|
context["cotton_slots"][self.component_key] = {}
|
||||||
|
|
||||||
|
# if self.slot_name not in context["cotton_slots"][self.component_key]:
|
||||||
|
# context["cotton_slots"][self.component_key][self.slot_name] = mark_safe(output)
|
||||||
|
context["cotton_slots"][self.component_key][self.slot_name] = mark_safe(output)
|
||||||
|
|
||||||
|
# context.push()
|
||||||
|
|
||||||
|
# todo add scoping by component
|
||||||
|
# context["cotton_slots"][self.component_key][self.slot_name] = mark_safe(output)
|
||||||
|
|
||||||
|
return ""
|
26
django_cotton/templatetags/cotton.py
Normal file
26
django_cotton/templatetags/cotton.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html_join
|
||||||
|
|
||||||
|
from django_cotton.templatetags._component import cotton_component
|
||||||
|
from django_cotton.templatetags._slot import cotton_slot
|
||||||
|
from django_cotton.templatetags._props import cotton_props
|
||||||
|
from django_cotton.templatetags._props_frame import cotton_props_frame
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
register.tag("cotton_component", cotton_component)
|
||||||
|
register.tag("cotton_slot", cotton_slot)
|
||||||
|
register.tag("cotton_props", cotton_props)
|
||||||
|
register.tag("cotton_props_frame", cotton_props_frame)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def merge(attrs, args):
|
||||||
|
# attrs is expected to be a dictionary of existing attributes
|
||||||
|
# args is a string of additional attributes to merge, e.g., "class:extra-class"
|
||||||
|
for arg in args.split(","):
|
||||||
|
key, value = arg.split(":", 1)
|
||||||
|
if key in attrs:
|
||||||
|
attrs[key] = value + " " + attrs[key]
|
||||||
|
else:
|
||||||
|
attrs[key] = value
|
||||||
|
return format_html_join(" ", '{0}="{1}"', attrs.items())
|
0
django_cotton/tests/__init__.py
Normal file
0
django_cotton/tests/__init__.py
Normal file
111
django_cotton/tests/test_cotton.py
Normal file
111
django_cotton/tests/test_cotton.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from django_cotton.tests.utils import get_compiled, get_rendered
|
||||||
|
|
||||||
|
|
||||||
|
class CottonTestCase(TestCase):
|
||||||
|
def test_parent_component_is_rendered(self):
|
||||||
|
response = self.client.get("/parent")
|
||||||
|
self.assertContains(response, '<div class="i-am-parent">')
|
||||||
|
|
||||||
|
def test_child_is_rendered(self):
|
||||||
|
response = self.client.get("/child")
|
||||||
|
self.assertContains(response, '<div class="i-am-parent">')
|
||||||
|
self.assertContains(response, '<div class="i-am-child">')
|
||||||
|
|
||||||
|
def test_self_closing_is_rendered(self):
|
||||||
|
response = self.client.get("/self-closing")
|
||||||
|
self.assertContains(response, '<div class="i-am-parent">')
|
||||||
|
|
||||||
|
def test_named_slots_correctly_display_in_loop(self):
|
||||||
|
response = self.client.get("/named-slot-in-loop")
|
||||||
|
self.assertContains(response, "item name: Item 1")
|
||||||
|
self.assertContains(response, "item name: Item 2")
|
||||||
|
self.assertContains(response, "item name: Item 3")
|
||||||
|
|
||||||
|
def test_attribute_passing(self):
|
||||||
|
response = self.client.get("/attribute-passing")
|
||||||
|
self.assertContains(
|
||||||
|
response, '<div and-another="woo1" attribute_1="hello" thirdforluck="yes">'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_attribute_merging(self):
|
||||||
|
response = self.client.get("/attribute-merging")
|
||||||
|
self.assertContains(
|
||||||
|
response, 'class="form-group another-class-with:colon extra-class"'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_django_syntax_decoding(self):
|
||||||
|
response = self.client.get("/django-syntax-decoding")
|
||||||
|
self.assertContains(response, "some-class")
|
||||||
|
|
||||||
|
def test_props_are_converted_to_props_frame_tags(self):
|
||||||
|
compiled = get_compiled(
|
||||||
|
"""
|
||||||
|
<c-props prop1="string with space" />
|
||||||
|
|
||||||
|
content
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEquals(
|
||||||
|
compiled,
|
||||||
|
"""{% cotton_props_frame prop1=prop1|default:"string with space" %}content{% endcotton_props_frame %}""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_attrs_do_not_contain_props(self):
|
||||||
|
response = self.client.get("/props-test")
|
||||||
|
self.assertContains(response, "attr1: 'im an attr'")
|
||||||
|
self.assertContains(response, "prop1: 'im a prop'")
|
||||||
|
self.assertContains(response, """attrs: 'attr1="im an attr"'""")
|
||||||
|
|
||||||
|
def test_strings_with_spaces_can_be_passed(self):
|
||||||
|
response = self.client.get("/string-with-spaces")
|
||||||
|
self.assertContains(response, "attr1: 'I have spaces'")
|
||||||
|
self.assertContains(response, "prop1: 'string with space'")
|
||||||
|
self.assertContains(response, "default_prop: 'default prop'")
|
||||||
|
self.assertContains(response, "named_slot: '")
|
||||||
|
self.assertContains(response, "named_slot with spaces")
|
||||||
|
self.assertContains(response, """attrs: 'attr1="I have spaces"'""")
|
||||||
|
|
||||||
|
def test_named_slots_dont_bleed_into_sibling_components(self):
|
||||||
|
html = """
|
||||||
|
<c-test-component>
|
||||||
|
component1
|
||||||
|
<c-slot name="named_slot">named slot 1</c-slot>
|
||||||
|
</c-test-component>
|
||||||
|
<c-test-component>
|
||||||
|
component2
|
||||||
|
</c-test-component>
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = get_rendered(html)
|
||||||
|
|
||||||
|
self.assertTrue("named_slot: 'named slot 1'" in rendered)
|
||||||
|
self.assertTrue("named_slot: ''" in rendered)
|
||||||
|
|
||||||
|
def test_template_variables_are_not_parsed(self):
|
||||||
|
html = """
|
||||||
|
<c-test-component attr1="variable" :attr2="variable">
|
||||||
|
<c-slot name="named_slot">
|
||||||
|
<a href="#" silica:click.prevent="variable = 'lineage'">test</a>
|
||||||
|
</c-slot>
|
||||||
|
</c-test-component>
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = get_rendered(html, {"variable": 1})
|
||||||
|
|
||||||
|
self.assertTrue("attr1: 'variable'" in rendered)
|
||||||
|
self.assertTrue("attr2: '1'" in rendered)
|
||||||
|
|
||||||
|
def test_int_attributes(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_none_attributes(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_list_attributes(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def quotes_inside_quotes(self):
|
||||||
|
pass
|
18
django_cotton/tests/utils.py
Normal file
18
django_cotton/tests/utils.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from django.template import Context, Template
|
||||||
|
|
||||||
|
from django_cotton.cotton_loader import Loader as CottonLoader
|
||||||
|
|
||||||
|
|
||||||
|
def get_compiled(template_string):
|
||||||
|
return CottonLoader(engine=None)._compile_template_from_string(
|
||||||
|
template_string, component_key="test_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rendered(template_string, context: dict = None):
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
compiled_string = get_compiled(template_string)
|
||||||
|
|
||||||
|
return Template(compiled_string).render(Context(context))
|
45
django_cotton/urls.py
Normal file
45
django_cotton/urls.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from . import views
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
app_name = "django_cotton"
|
||||||
|
|
||||||
|
|
||||||
|
class NamedSlotInLoop(TemplateView):
|
||||||
|
template_name = "named_slot_in_loop.cotton.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{"name": "Item 1"},
|
||||||
|
{"name": "Item 2"},
|
||||||
|
{"name": "Item 3"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("parent", TemplateView.as_view(template_name="parent_test.cotton.html")),
|
||||||
|
path("child", TemplateView.as_view(template_name="child_test.cotton.html")),
|
||||||
|
path(
|
||||||
|
"self-closing",
|
||||||
|
TemplateView.as_view(template_name="self_closing_test.cotton.html"),
|
||||||
|
),
|
||||||
|
path("include", TemplateView.as_view(template_name="cotton_include.cotton.html")),
|
||||||
|
path("playground", TemplateView.as_view(template_name="playground.cotton.html")),
|
||||||
|
path("tag", TemplateView.as_view(template_name="tag.cotton.html")),
|
||||||
|
path("named-slot-in-loop", NamedSlotInLoop.as_view()),
|
||||||
|
path("test/compiled-cotton", views.compiled_cotton_test_view),
|
||||||
|
path("test/cotton", views.cotton_test_view),
|
||||||
|
path("test/native-extends", views.native_extends_test_view),
|
||||||
|
path("test/native-include", views.native_include_test_view),
|
||||||
|
path("attribute-merging", views.attribute_merging_test_view),
|
||||||
|
path("attribute-passing", views.attribute_passing_test_view),
|
||||||
|
path("django-syntax-decoding", views.django_syntax_decoding_test_view),
|
||||||
|
path(
|
||||||
|
"string-with-spaces",
|
||||||
|
TemplateView.as_view(template_name="string_with_spaces.cotton.html"),
|
||||||
|
),
|
||||||
|
path("props-test", TemplateView.as_view(template_name="props_test.cotton.html")),
|
||||||
|
path("variable-parsing", views.variable_parsing_test_view),
|
||||||
|
]
|
40
django_cotton/views.py
Normal file
40
django_cotton/views.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# benchmark tests
|
||||||
|
|
||||||
|
|
||||||
|
def compiled_cotton_test_view(request):
|
||||||
|
return render(request, "compiled_cotton_test.html")
|
||||||
|
|
||||||
|
|
||||||
|
def cotton_test_view(request):
|
||||||
|
return render(request, "cotton_test.cotton.html")
|
||||||
|
|
||||||
|
|
||||||
|
def native_extends_test_view(request):
|
||||||
|
return render(request, "native_extends_test.html")
|
||||||
|
|
||||||
|
|
||||||
|
def native_include_test_view(request):
|
||||||
|
return render(request, "native_include_test.html")
|
||||||
|
|
||||||
|
|
||||||
|
# Django tests
|
||||||
|
|
||||||
|
|
||||||
|
def attribute_merging_test_view(request):
|
||||||
|
return render(request, "attribute_merging_test.cotton.html")
|
||||||
|
|
||||||
|
|
||||||
|
def attribute_passing_test_view(request):
|
||||||
|
return render(request, "attribute_passing_test.cotton.html")
|
||||||
|
|
||||||
|
|
||||||
|
def django_syntax_decoding_test_view(request):
|
||||||
|
return render(request, "django_syntax_decoding_test.cotton.html")
|
||||||
|
|
||||||
|
|
||||||
|
def variable_parsing_test_view(request):
|
||||||
|
return render(
|
||||||
|
request, "variable_parsing_test.cotton.html", {"variable": "some-class"}
|
||||||
|
)
|
16
django_cotton/wsgi.py
Normal file
16
django_cotton/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for django_cotton project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
39
docs/docker/Dockerfile
Normal file
39
docs/docker/Dockerfile
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
ARG PLATFORM=linux/amd64
|
||||||
|
|
||||||
|
# ---- Tailwind Build Stage (build is quicker like this vs installing npm, node separately) ----
|
||||||
|
FROM node:16-slim AS build_tailwind
|
||||||
|
WORKDIR /css
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN ["npx", "tailwindcss", "-o", "./docs_project/static/app.css"]
|
||||||
|
|
||||||
|
# Use an official Python runtime as a base image
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Keep logs unbuffered
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
# Set the working directory in the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
RUN pip install --upgrade pip \
|
||||||
|
&& pip install poetry
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
chromium \
|
||||||
|
chromium-driver \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy only dependencies definition to the docker image
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# Install project dependencies
|
||||||
|
RUN poetry config virtualenvs.create false \
|
||||||
|
&& poetry install
|
||||||
|
|
||||||
|
RUN SECRET_KEY=dummy STATIC_URL='/staticfiles/' python manage.py collectstatic --noinput --verbosity 2
|
||||||
|
|
||||||
|
CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]
|
16
docs/docker/bin/build.sh
Executable file
16
docs/docker/bin/build.sh
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# if arg exists 'mac', then build for mac
|
||||||
|
if [ "$1" = "mac" ]; then
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
else
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
|
||||||
|
docker build --no-cache --build-arg PLATFORM=$PLATFORM -f ../Dockerfile ../../docs_project -t cotton-docs-app
|
4
docs/docker/bin/manage.sh
Executable file
4
docs/docker/bin/manage.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Start a new login shell to preserve history and execute the Docker command
|
||||||
|
bash -l -c "docker exec -it cotton-docs-web python manage.py $*"
|
9
docs/docker/bin/run-dev.sh
Executable file
9
docs/docker/bin/run-dev.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
|
||||||
|
docker compose -f ../docker-compose.yaml up "$@"
|
9
docs/docker/bin/shell.sh
Executable file
9
docs/docker/bin/shell.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR/..
|
||||||
|
|
||||||
|
docker exec -it cotton-docs-web python manage.py shell
|
9
docs/docker/bin/stop.sh
Executable file
9
docs/docker/bin/stop.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine script directory
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
|
# Change directory to project directory
|
||||||
|
cd $SCRIPT_DIR/..
|
||||||
|
|
||||||
|
docker compose -f etc/docker/docker-compose.yaml -f etc/docker/docker-compose.dev.yaml down $*
|
3
docs/docker/bin/terminal.sh
Executable file
3
docs/docker/bin/terminal.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
docker exec -it cotton-docs-web bash
|
3
docs/docker/bin/test.sh
Executable file
3
docs/docker/bin/test.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
docker exec -t cotton-docs-web python manage.py test $*
|
28
docs/docker/docker-compose.yaml
Normal file
28
docs/docker/docker-compose.yaml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
cotton-docs-web:
|
||||||
|
container_name: cotton-docs-web
|
||||||
|
restart: always
|
||||||
|
image: cotton-docs-app
|
||||||
|
working_dir: /app
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
environment:
|
||||||
|
- DEBUG=True
|
||||||
|
volumes:
|
||||||
|
- ../docs_project:/app
|
||||||
|
- ../../django_cotton:/app/django_cotton
|
||||||
|
ports:
|
||||||
|
- 8002:8000
|
||||||
|
|
||||||
|
cotton-docs-tailwind:
|
||||||
|
container_name: cotton-docs-tailwind
|
||||||
|
stop_signal: SIGINT
|
||||||
|
image: node:16
|
||||||
|
working_dir: /app
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- ../docs_project:/app
|
||||||
|
# call the tailwind build before the watch
|
||||||
|
# command: /bin/sh -c "npm install && npx tailwindcss -c ./tailwind.config.js -o ./luma/static/app.css && npx tailwindcss -c ./tailwind.config.js -o ./luma/static/app.css --watch"
|
||||||
|
command: /bin/sh -c "trap 'exit' INT; npm install && npx tailwindcss -c ./tailwind.config.js -o ./docs_project/static/app.css && npx tailwindcss -c ./tailwind.config.js -o ./docs_project/static/app.css --watch"
|
45
docs/docs_project/DockerfileTmp
Normal file
45
docs/docs_project/DockerfileTmp
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
ARG PLATFORM=linux/amd64
|
||||||
|
|
||||||
|
# ---- Tailwind Build Stage (build is quicker like this vs installing npm, node separately) ----
|
||||||
|
FROM node:16-slim AS build_tailwind
|
||||||
|
WORKDIR /css
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN ["npx", "tailwindcss", "-o", "./docs_project/static/app.css"]
|
||||||
|
|
||||||
|
# Use an official Python runtime as a base image
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Keep logs unbuffered
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
# Set the working directory in the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
RUN pip install --upgrade pip \
|
||||||
|
&& pip install poetry
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
chromium \
|
||||||
|
chromium-driver \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy only dependencies definition to the docker image
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# Copy static files from the build_tailwind stage
|
||||||
|
COPY --from=build_tailwind /css/docs_project/static/app.css ./docs_project/static/app.css
|
||||||
|
|
||||||
|
# Install project dependencies
|
||||||
|
RUN poetry config virtualenvs.create false \
|
||||||
|
&& poetry install
|
||||||
|
|
||||||
|
RUN SECRET_KEY=dummy STATIC_URL='/staticfiles/' python manage.py collectstatic --noinput --verbosity 2
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
ENV PORT 8000
|
||||||
|
|
||||||
|
CMD ["gunicorn", "docs_project.wsgi:application", "--bind", "0.0.0.0:8000"]
|
139
docs/docs_project/README.md
Normal file
139
docs/docs_project/README.md
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
# Cotton
|
||||||
|
|
||||||
|
Bringing component-based design to Django templates.
|
||||||
|
|
||||||
|
<a href="https://www.django-cotton.com" target="_blank">Document site</a>
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Cotton enhances Django templates by allowing component-based design, making UI composition more efficient and reusable. It integrates seamlessly with Tailwind CSS and retains full compatibility with native Django template features.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
- **Rapid UI Composition:** Efficiently compose and reuse UI components.
|
||||||
|
- **Tailwind CSS Harmony:** Integrates with Tailwind's utility-first approach.
|
||||||
|
- **Interoperable with Django:** Enhances Django templates without replacing them.
|
||||||
|
- **Semantic Syntax:** HTML-like syntax for better code editor support.
|
||||||
|
- **Minimal Overhead:** Compiles to native Django components with automatic caching.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
To install Cotton, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install django-cotton
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update your `settings.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
...
|
||||||
|
'django_cotton',
|
||||||
|
]
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': ['your_project/templates'],
|
||||||
|
'APP_DIRS': False,
|
||||||
|
'OPTIONS': {
|
||||||
|
'loaders': [
|
||||||
|
'django_cotton.template.loaders.CottonLoader',
|
||||||
|
# continue with default loaders:
|
||||||
|
# "django.template.loaders.filesystem.Loader",
|
||||||
|
# "django.template.loaders.app_directories.Loader",
|
||||||
|
],
|
||||||
|
'builtins': [
|
||||||
|
'django_cotton.templatetags.cotton',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quickstart
|
||||||
|
Create a new directory in your templates directory called `cotton`. Inside this directory, create a new file called `card.cotton.html` with the following content:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="bg-white shadow rounded border p-4">
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
<p>{{ slot }}</p>
|
||||||
|
<button href="{% url url %}">Read more</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a view with a template. Views that contain Cotton components must also use the `.cotton.html` extension:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# views.py
|
||||||
|
def dashboard_view(request):
|
||||||
|
return render(request, "dashboard.cotton.html")
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- templates/dashboard.cotton.html -->
|
||||||
|
<c-card title="Trees" url="trees">
|
||||||
|
We have the best trees
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="Spades" url="spades">
|
||||||
|
The best spades in the land
|
||||||
|
</c-card>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage Basics
|
||||||
|
- **Template Extensions:** View templates including Cotton components should use the `.cotton.html` extension.
|
||||||
|
- **Component Placement:** Components should be placed in the `templates/cotton` folder.
|
||||||
|
- **Naming Conventions:**
|
||||||
|
- Component filenames use snake_case: `my_component.cotton.html`
|
||||||
|
- Components are called using kebab-case: `<c-my-component />`
|
||||||
|
|
||||||
|
### Example
|
||||||
|
A minimal example using Cotton components:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- my_component.cotton.html -->
|
||||||
|
{{ slot }}
|
||||||
|
|
||||||
|
<!-- my_view.cotton.html -->
|
||||||
|
<c-my-component>
|
||||||
|
<p>Some content</p>
|
||||||
|
</c-my-component>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attributes and Slots
|
||||||
|
Components can accept attributes and named slots for flexible content and behavior customization:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- weather.cotton.html -->
|
||||||
|
<p>It's {{ temperature }}<sup>{{ unit }}</sup> and the condition is {{ condition }}.</p>
|
||||||
|
|
||||||
|
<!-- view.cotton.html -->
|
||||||
|
<c-weather temperature="23" unit="c" condition="windy"></c-weather>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passing Variables
|
||||||
|
To pass a variable from the parent's context, prepend the attribute with a `:`.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- view.cotton.html -->
|
||||||
|
<c-weather :unit="unit"></c-weather>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Named Slots
|
||||||
|
```html
|
||||||
|
<!-- weather_card.cotton.html -->
|
||||||
|
<div class="flex ...">
|
||||||
|
<h2>{{ day }}:</h2> {{ icon }} {{ label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- view.cotton.html -->
|
||||||
|
<c-weather-card day="Tuesday">
|
||||||
|
<c-slot name="icon">
|
||||||
|
<svg>...</svg>
|
||||||
|
</c-slot>
|
||||||
|
<c-slot name="label">
|
||||||
|
<h2 class="text-yellow-500">Sunny</h2>
|
||||||
|
</c-slot>
|
||||||
|
</c-weather-card>
|
||||||
|
```
|
1
docs/docs_project/django_cotton/__init__.py
Normal file
1
docs/docs_project/django_cotton/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
_with_prop_prefix = "cotton_with_prop_"
|
60
docs/docs_project/django_cotton/apps.py
Normal file
60
docs/docs_project/django_cotton/apps.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template import engines
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
class CottonConfig(AppConfig):
|
||||||
|
name = "django_cotton"
|
||||||
|
verbose_name = "Cotton"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
self._modify_templates_settings()
|
||||||
|
self._add_builtin_template_tag()
|
||||||
|
|
||||||
|
def _modify_templates_settings(self):
|
||||||
|
modified = False
|
||||||
|
for template in settings.TEMPLATES:
|
||||||
|
if not template.get("APP_DIRS", True):
|
||||||
|
# If APP_DIRS is explicitly set to False, we assume the user
|
||||||
|
# has a custom setup and do not modify the settings and provide tutorial instead.
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Example modification: Set APP_DIRS to False
|
||||||
|
template["APP_DIRS"] = False
|
||||||
|
|
||||||
|
# Add your custom template loader
|
||||||
|
if "OPTIONS" not in template:
|
||||||
|
template["OPTIONS"] = {}
|
||||||
|
if "loaders" not in template["OPTIONS"]:
|
||||||
|
template["OPTIONS"]["loaders"] = []
|
||||||
|
|
||||||
|
# Add django_cotton loader
|
||||||
|
if "django_cotton.cotton_loader.Loader" not in template["OPTIONS"]["loaders"]:
|
||||||
|
template["OPTIONS"]["loaders"].insert(
|
||||||
|
0, "django_cotton.cotton_loader.Loader"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure default loaders are present, then add your custom loader
|
||||||
|
if "django.template.loaders.filesystem.Loader" not in template["OPTIONS"]["loaders"]:
|
||||||
|
template["OPTIONS"]["loaders"].append(
|
||||||
|
"django.template.loaders.filesystem.Loader"
|
||||||
|
)
|
||||||
|
if "django.template.loaders.app_directories.Loader" not in template["OPTIONS"]["loaders"]:
|
||||||
|
template["OPTIONS"]["loaders"].append(
|
||||||
|
"django.template.loaders.app_directories.Loader"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Specify TEMPLATE_DIRS if necessary
|
||||||
|
# template['DIRS'] += ['path/to/your/templates']
|
||||||
|
|
||||||
|
modified = True
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
print("TEMPLATES setting modified by Django Cotton.")
|
||||||
|
|
||||||
|
def _add_builtin_template_tag(self):
|
||||||
|
"""Add a custom template tag to the built-ins."""
|
||||||
|
builtins = engines["django"].engine.builtins
|
||||||
|
custom_tag_lib = "django_cotton.templatetags.cotton"
|
||||||
|
if custom_tag_lib not in builtins:
|
||||||
|
builtins.append(custom_tag_lib)
|
291
docs/docs_project/django_cotton/cotton_loader.py
Executable file
291
docs/docs_project/django_cotton/cotton_loader.py
Executable file
|
@ -0,0 +1,291 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import hashlib
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from django.template.loaders.base import Loader as BaseLoader
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
|
from django.template import TemplateDoesNotExist
|
||||||
|
from django.utils._os import safe_join
|
||||||
|
from django.template import Template
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.template import Origin
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||||
|
|
||||||
|
|
||||||
|
class Loader(BaseLoader):
|
||||||
|
is_usable = True
|
||||||
|
|
||||||
|
def __init__(self, engine, dirs=None):
|
||||||
|
super().__init__(engine)
|
||||||
|
self.cache_handler = CottonTemplateCacheHandler()
|
||||||
|
self.dirs = dirs
|
||||||
|
self.django_syntax_placeholders = []
|
||||||
|
|
||||||
|
def get_template_from_string(self, template_string):
|
||||||
|
"""Create and return a Template object from a string. Used primarily for testing."""
|
||||||
|
return Template(template_string, engine=self.engine)
|
||||||
|
|
||||||
|
def get_contents(self, origin):
|
||||||
|
# check if file exists, whilst getting the mtime for cache key
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(origin.name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise TemplateDoesNotExist(origin)
|
||||||
|
|
||||||
|
# check and return cached template
|
||||||
|
cache_key = self.cache_handler.get_cache_key(origin.template_name, mtime)
|
||||||
|
cached_content = self.cache_handler.get_cached_template(cache_key)
|
||||||
|
if cached_content is not None:
|
||||||
|
return cached_content
|
||||||
|
|
||||||
|
# If not cached, process the template
|
||||||
|
template_string = self._get_template_string(origin.name)
|
||||||
|
|
||||||
|
# We need to provide a key to the current view or component (in this case, view) so that we can namespace
|
||||||
|
# slot data, preventing bleeding and ensure component's clear only data in the context applicable to itself
|
||||||
|
# in this case, we're top level, likely in a view so we use the view template name as the key
|
||||||
|
component_key = (
|
||||||
|
origin.template_name.lstrip("cotton/")
|
||||||
|
.rstrip(".cotton.html")
|
||||||
|
.replace("/", ".")
|
||||||
|
)
|
||||||
|
|
||||||
|
compiled_template = self._compile_template_from_string(
|
||||||
|
template_string, component_key
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cache the processed template
|
||||||
|
self.cache_handler.cache_template(cache_key, compiled_template)
|
||||||
|
|
||||||
|
return compiled_template
|
||||||
|
|
||||||
|
def _replace_syntax_with_placeholders(self, content):
|
||||||
|
"""# replace {% ... %} and {{ ... }} with placeholders so they dont get touched
|
||||||
|
or encoded by bs4. Store them to later switch them back in after transformation.
|
||||||
|
"""
|
||||||
|
self.django_syntax_placeholders = []
|
||||||
|
|
||||||
|
# First handle cotton_verbatim blocks, this is designed to preserve and display cotton syntax,
|
||||||
|
# akin to the verbatim tag in Django.
|
||||||
|
def replace_cotton_verbatim(match):
|
||||||
|
inner_content = match.group(
|
||||||
|
1
|
||||||
|
) # Get the inner content without the cotton_verbatim tags
|
||||||
|
self.django_syntax_placeholders.append(inner_content)
|
||||||
|
return f"__django_syntax__{len(self.django_syntax_placeholders)}__"
|
||||||
|
|
||||||
|
# Replace cotton_verbatim blocks, capturing inner content
|
||||||
|
content = re.sub(
|
||||||
|
r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}",
|
||||||
|
replace_cotton_verbatim,
|
||||||
|
content,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r"\{%.*?%\}",
|
||||||
|
lambda x: self.django_syntax_placeholders.append(x.group(0))
|
||||||
|
or f"__django_syntax__{len(self.django_syntax_placeholders)}__",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
content = re.sub(
|
||||||
|
r"\{\{.*?\}\}",
|
||||||
|
lambda x: self.django_syntax_placeholders.append(x.group(0))
|
||||||
|
or f"__django_syntax__{len(self.django_syntax_placeholders)}__",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _replace_placeholders_with_syntax(self, content):
|
||||||
|
"""After modifying the content, replace the placeholders with the django template tags and variables."""
|
||||||
|
for i, placeholder in enumerate(self.django_syntax_placeholders, 1):
|
||||||
|
content = content.replace(f"__django_syntax__{i}__", placeholder)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _get_template_string(self, template_name):
|
||||||
|
try:
|
||||||
|
with open(template_name, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise TemplateDoesNotExist(template_name)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _compile_template_from_string(self, content, component_key):
|
||||||
|
content = self._replace_syntax_with_placeholders(content)
|
||||||
|
content = self._compile_cotton_to_django(content, component_key)
|
||||||
|
content = self._replace_placeholders_with_syntax(content)
|
||||||
|
content = self._revert_bs4_attribute_empty_attribute_fixing(content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _revert_bs4_attribute_empty_attribute_fixing(self, contents):
|
||||||
|
"""Django's template parser adds ="" to empty attribute-like parts in any html-like node, i.e. <div {{ something }}> gets
|
||||||
|
compiled to <div {{ something }}=""> Then if 'something' is holding attributes sets, the last attribute value is
|
||||||
|
not quoted. i.e. model=test not model="test"."""
|
||||||
|
cleaned_content = re.sub('}}=""', "}}", contents)
|
||||||
|
return cleaned_content
|
||||||
|
|
||||||
|
def get_dirs(self):
|
||||||
|
return self.dirs if self.dirs is not None else self.engine.dirs
|
||||||
|
|
||||||
|
def get_template_sources(self, template_name):
|
||||||
|
"""Return an Origin object pointing to an absolute path in each directory
|
||||||
|
in template_dirs. For security reasons, if a path doesn't lie inside
|
||||||
|
one of the template_dirs it is excluded from the result set."""
|
||||||
|
if template_name.endswith(".cotton.html"):
|
||||||
|
for template_dir in self.get_dirs():
|
||||||
|
try:
|
||||||
|
name = safe_join(template_dir, template_name)
|
||||||
|
except SuspiciousFileOperation:
|
||||||
|
# The joined path was located outside of this template_dir
|
||||||
|
# (it might be inside another one, so this isn't fatal).
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield Origin(
|
||||||
|
name=name,
|
||||||
|
template_name=template_name,
|
||||||
|
loader=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compile_cotton_to_django(self, html_content, component_key):
|
||||||
|
"""Convert cotton <c-* syntax to {%."""
|
||||||
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
|
|
||||||
|
soup = self._wrap_with_cotton_props_frame(soup)
|
||||||
|
self._transform_components(soup, component_key)
|
||||||
|
|
||||||
|
return str(soup)
|
||||||
|
|
||||||
|
def _transform_prop_tags(self, soup):
|
||||||
|
c_props = soup.find_all("c-props")
|
||||||
|
|
||||||
|
for tag in c_props:
|
||||||
|
# Build the cotton_props tag string
|
||||||
|
props_list = []
|
||||||
|
for prop, value in tag.attrs.items():
|
||||||
|
if value is None:
|
||||||
|
props_list.append(prop)
|
||||||
|
else:
|
||||||
|
props_list.append(f'{prop}="{value}"')
|
||||||
|
|
||||||
|
cotton_props_str = "{% cotton_props " + " ".join(props_list) + " %}"
|
||||||
|
|
||||||
|
# Replace the <c-props> tag with the cotton_props string
|
||||||
|
tag.replace_with(cotton_props_str)
|
||||||
|
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def _wrap_with_cotton_props_frame(self, soup):
|
||||||
|
"""Wrap content with {% cotton_props_frame %} to be able to govern props and attributes. In order to recognise
|
||||||
|
props defined in a component and also have them available in context, we wrap the entire contents in another
|
||||||
|
component: cotton_props_frame."""
|
||||||
|
props_with_defaults = []
|
||||||
|
c_props = soup.find("c-props")
|
||||||
|
|
||||||
|
# parse c-props tag to extract properties and defaults
|
||||||
|
if c_props:
|
||||||
|
props_with_defaults = []
|
||||||
|
for prop, value in c_props.attrs.items():
|
||||||
|
if value is None:
|
||||||
|
props_with_defaults.append(f"{prop}={prop}")
|
||||||
|
else:
|
||||||
|
# Assuming value is already a string that represents the default value
|
||||||
|
props_with_defaults.append(f'{prop}={prop}|default:"{value}"')
|
||||||
|
|
||||||
|
c_props.decompose()
|
||||||
|
|
||||||
|
# Construct the {% with %} opening tag
|
||||||
|
opening = "{% cotton_props_frame " + " ".join(props_with_defaults) + " %}"
|
||||||
|
closing = "{% endcotton_props_frame %}"
|
||||||
|
|
||||||
|
# Convert the remaining soup back to a string and wrap it within {% with %} block
|
||||||
|
wrapped_content = opening + str(soup).strip() + closing
|
||||||
|
|
||||||
|
# Since we can't replace the soup object itself, we create new soup instead
|
||||||
|
new_soup = BeautifulSoup(wrapped_content, "html.parser")
|
||||||
|
|
||||||
|
return new_soup
|
||||||
|
|
||||||
|
def _transform_named_slot(self, slot_tag, component_key):
|
||||||
|
"""Replace <c-slot> tags with the {% cotton_slot %} template tag"""
|
||||||
|
# for c_slot in soup.find_all("c-slot"):
|
||||||
|
slot_name = slot_tag.get("name", "").strip()
|
||||||
|
inner_html = "".join(str(content) for content in slot_tag.contents)
|
||||||
|
|
||||||
|
# Check and process any components in the slot content
|
||||||
|
|
||||||
|
slot_soup = BeautifulSoup(inner_html, "html.parser")
|
||||||
|
self._transform_components(slot_soup, component_key)
|
||||||
|
|
||||||
|
cotton_slot_tag = f"{{% cotton_slot {slot_name} {component_key} %}}{str(slot_soup)}{{% end_cotton_slot %}}"
|
||||||
|
|
||||||
|
slot_tag.replace_with(BeautifulSoup(cotton_slot_tag, "html.parser"))
|
||||||
|
|
||||||
|
def _transform_components(self, soup, component_key):
|
||||||
|
"""Replace <c-[component path]> tags with the {% cotton_component %} template tag"""
|
||||||
|
for tag in soup.find_all(re.compile("^c-"), recursive=True):
|
||||||
|
if tag.name == "c-slot":
|
||||||
|
self._transform_named_slot(tag, component_key)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
component_name = tag.name[2:]
|
||||||
|
|
||||||
|
# Convert dot notation to path structure and replace hyphens with underscores
|
||||||
|
path = component_name.replace(".", "/").replace("-", "_")
|
||||||
|
|
||||||
|
# Construct the opening tag
|
||||||
|
opening_tag = f"{{% cotton_component {'cotton/{}.cotton.html'.format(path)} {component_name} "
|
||||||
|
for attr, value in tag.attrs.items():
|
||||||
|
if attr == "class":
|
||||||
|
value = " ".join(value)
|
||||||
|
opening_tag += ' {}="{}"'.format(attr, value)
|
||||||
|
opening_tag += " %}"
|
||||||
|
|
||||||
|
# Construct the closing tag
|
||||||
|
closing_tag = "{% end_cotton_component %}"
|
||||||
|
|
||||||
|
if tag.contents:
|
||||||
|
tag_soup = BeautifulSoup(tag.decode_contents(), "html.parser")
|
||||||
|
self._transform_components(tag_soup, component_name)
|
||||||
|
|
||||||
|
# Create new content with opening tag, tag content, and closing tag
|
||||||
|
new_content = opening_tag + str(tag_soup) + closing_tag
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Create new content with opening tag and closing tag
|
||||||
|
new_content = opening_tag + closing_tag
|
||||||
|
|
||||||
|
# Replace the original tag with the new content
|
||||||
|
new_soup = BeautifulSoup(new_content, "html.parser")
|
||||||
|
tag.replace_with(new_soup)
|
||||||
|
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
class CottonTemplateCacheHandler:
|
||||||
|
"""Handles caching of cotton templates so the html parsing is only done on first load of each view or component."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = getattr(settings, "TEMPLATE_CACHING_ENABLED", True)
|
||||||
|
|
||||||
|
def get_cache_key(self, template_name, mtime):
|
||||||
|
template_hash = hashlib.sha256(template_name.encode()).hexdigest()
|
||||||
|
return f"cotton_cache_{template_hash}_{mtime}"
|
||||||
|
|
||||||
|
def get_cached_template(self, cache_key):
|
||||||
|
if not self.enabled:
|
||||||
|
return None
|
||||||
|
return cache.get(cache_key)
|
||||||
|
|
||||||
|
def cache_template(self, cache_key, content, timeout=None):
|
||||||
|
if self.enabled:
|
||||||
|
cache.set(cache_key, content, timeout=timeout)
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-merges-attributes class="extra-class" silica:model="test" another="test">
|
||||||
|
ss
|
||||||
|
</c-merges-attributes>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-receives-attributes attribute_1="hello" and-another="woo1" thirdForLuck="yes">
|
||||||
|
ss
|
||||||
|
</c-receives-attributes>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-parent>
|
||||||
|
<c-child>d</c-child>
|
||||||
|
</c-parent>
|
|
@ -0,0 +1 @@
|
||||||
|
<div class="i-am-child"></div>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div>
|
||||||
|
Header:
|
||||||
|
{{ header }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Content:
|
||||||
|
{{ slot }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div {{ attrs_dict|merge:'class:form-group another-class-with:colon' }}>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
{{ name }}
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="i-am-parent">
|
||||||
|
{{slot}}
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div {{ attrs }}>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<c-props prop1 default_prop="default prop" />
|
||||||
|
|
||||||
|
<p>slot: '{{ slot }}'</p>
|
||||||
|
|
||||||
|
<p>attr1: '{{ attr1 }}'</p>
|
||||||
|
<p>attr2: '{{ attr2 }}'</p>
|
||||||
|
|
||||||
|
<p>prop1: '{{ prop1 }}'</p>
|
||||||
|
<p>default_prop: '{{ default_prop }}'</p>
|
||||||
|
|
||||||
|
<p>named_slot: '{{ named_slot }}'</p>
|
||||||
|
|
||||||
|
<p>attrs: '{{ attrs }}'</p>
|
|
@ -0,0 +1 @@
|
||||||
|
<div class="{% if 1 < 2 %} some-class {% endif %}">Hello, World!</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<c-parent>
|
||||||
|
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
|
||||||
|
</c-parent>
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% for item in items %}
|
||||||
|
<c-named-slot-component>
|
||||||
|
<c-slot name="name">
|
||||||
|
item name: {{ item.name }}
|
||||||
|
</c-slot>
|
||||||
|
</c-named-slot-component>
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1 @@
|
||||||
|
<c-parent></c-parent>
|
|
@ -0,0 +1,4 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
|
<c-parent/>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<c-test-component prop1="string with space" attr1="I have spaces">
|
||||||
|
<c-slot name="named_slot">
|
||||||
|
named_slot with spaces
|
||||||
|
</c-slot>
|
||||||
|
</c-test-component>
|
|
@ -0,0 +1,2 @@
|
||||||
|
<c-test-component attr1="variable" :attr2="variable">
|
||||||
|
</c-test-component>
|
0
docs/docs_project/django_cotton/templatetags/__init__.py
Normal file
0
docs/docs_project/django_cotton/templatetags/__init__.py
Normal file
68
docs/docs_project/django_cotton/templatetags/_component.py
Normal file
68
docs/docs_project/django_cotton/templatetags/_component.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from django import template
|
||||||
|
from django.template import Node
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
|
||||||
|
def cotton_component(parser, token):
|
||||||
|
bits = token.split_contents()
|
||||||
|
tag_name = bits[0]
|
||||||
|
template_path = bits[1]
|
||||||
|
component_key = bits[2]
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
for bit in bits[3:]:
|
||||||
|
key, value = bit.split("=")
|
||||||
|
if key.startswith(":"): # Detect variables
|
||||||
|
key = key[1:] # Remove ':' prefix
|
||||||
|
value = value.strip("'\"") # Remove quotes
|
||||||
|
kwargs[key] = template.Variable(value) # Treat as a variable
|
||||||
|
else:
|
||||||
|
kwargs[key] = value.strip("'\"") # Treat as a literal string
|
||||||
|
|
||||||
|
nodelist = parser.parse(("end_cotton_component",))
|
||||||
|
parser.delete_first_token()
|
||||||
|
|
||||||
|
return CottonComponentNode(nodelist, template_path, component_key, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CottonComponentNode(Node):
|
||||||
|
def __init__(self, nodelist, template_path, component_key, kwargs):
|
||||||
|
self.nodelist = nodelist
|
||||||
|
self.template_path = template_path
|
||||||
|
self.component_key = component_key
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
local_context = context.flatten()
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
for key, value in self.kwargs.items():
|
||||||
|
if isinstance(value, template.Variable): # Resolve variables
|
||||||
|
try:
|
||||||
|
resolved_value = value.resolve(context)
|
||||||
|
attrs[key] = resolved_value
|
||||||
|
except template.VariableDoesNotExist:
|
||||||
|
pass # Handle variable not found, if necessary
|
||||||
|
else:
|
||||||
|
attrs[key] = value # Use literal string
|
||||||
|
|
||||||
|
# Add the remainder as the default slot
|
||||||
|
rendered = self.nodelist.render(context)
|
||||||
|
local_context.update({"slot": rendered})
|
||||||
|
|
||||||
|
slots = context.get("cotton_slots", {})
|
||||||
|
component_slots = slots.get(self.component_key, {})
|
||||||
|
|
||||||
|
local_context.update(component_slots)
|
||||||
|
local_context.update(attrs)
|
||||||
|
local_context.update({"attrs_dict": attrs})
|
||||||
|
|
||||||
|
rendered = render_to_string(self.template_path, local_context)
|
||||||
|
|
||||||
|
# Now reset the component's slots in context to prevent bleeding
|
||||||
|
if self.component_key in slots:
|
||||||
|
slots[self.component_key] = {}
|
||||||
|
|
||||||
|
context.update({"cotton_slots": slots})
|
||||||
|
|
||||||
|
return rendered
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue