touchable-templates 2026.2.3a0__tar.gz → 2026.2.5a0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- touchable_templates-2026.2.5a0/.python-version +1 -0
- {touchable_templates-2026.2.3a0 → touchable_templates-2026.2.5a0}/PKG-INFO +1 -1
- {touchable_templates-2026.2.3a0 → touchable_templates-2026.2.5a0}/pyproject.toml +3 -8
- touchable_templates-2026.2.5a0/src/touchable_templates/__init__.py +1 -0
- touchable_templates-2026.2.5a0/src/touchable_templates/apps.py +5 -0
- touchable_templates-2026.2.5a0/src/touchable_templates/django/loader.py +112 -0
- touchable_templates-2026.2.5a0/src/touchable_templates/django/middleware.py +52 -0
- touchable_templates-2026.2.5a0/src/touchable_templates/static/js/touchable_templates.js +66 -0
- touchable_templates-2026.2.5a0/uv.lock +45 -0
- {touchable_templates-2026.2.3a0 → touchable_templates-2026.2.5a0}/.gitignore +0 -0
- {touchable_templates-2026.2.3a0 → touchable_templates-2026.2.5a0}/LICENSE +0 -0
- {touchable_templates-2026.2.3a0 → touchable_templates-2026.2.5a0}/README.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "touchable-templates"
|
|
3
|
-
version = "2026.02.
|
|
3
|
+
version = "2026.02.5a"
|
|
4
4
|
description = "A dev tool that magically opens templates and partials in your IDE"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -22,10 +22,5 @@ url = "https://test.pypi.org/simple/"
|
|
|
22
22
|
publish-url = "https://test.pypi.org/legacy/"
|
|
23
23
|
explicit = true
|
|
24
24
|
|
|
25
|
-
[tool.hatch.build]
|
|
26
|
-
|
|
27
|
-
"touchable_templates/static/**",
|
|
28
|
-
]
|
|
29
|
-
# [tool.hatch.build.targets.wheel]
|
|
30
|
-
# packages = ["touchable_templates"]
|
|
31
|
-
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/touchable_templates"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Touchable Templates package initializer."""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from django.template.loaders.filesystem import Loader as FileSystemLoader
|
|
2
|
+
from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
import os
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
IDE_TO_URI_MAPPER = {
|
|
10
|
+
"atom": "atom://core/open/file?filename=",
|
|
11
|
+
"codelite": "codelite://open?file=",
|
|
12
|
+
"cursor": "cursor://file/",
|
|
13
|
+
"emacs": "emacs://open?url=",
|
|
14
|
+
"espresso": "x-espresso://open?filepath=",
|
|
15
|
+
"idea": "idea://open?file=",
|
|
16
|
+
"macvim": "mvim://open/?url=",
|
|
17
|
+
"netbeans": "netbeans://open/?f=",
|
|
18
|
+
"nova": "nova://open?path=",
|
|
19
|
+
"pycharm": "pycharm://open?file=",
|
|
20
|
+
"sublime": "subl://open?url=",
|
|
21
|
+
"textmate": "txmt://open?url=",
|
|
22
|
+
"vscode": "vscode://file/",
|
|
23
|
+
"vscode-insiders": "vscode-insiders://file/",
|
|
24
|
+
"vscode-insiders-remote": "vscode-insiders://vscode-remote/",
|
|
25
|
+
"vscode-remote": "vscode://vscode-remote/",
|
|
26
|
+
"vscodium": "vscodium://file/",
|
|
27
|
+
"xdebug": "xdebug://",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_setting_or_env(name):
|
|
32
|
+
# Prefer Django setting, fall back to environment variable
|
|
33
|
+
return getattr(settings, name, None) or os.environ.get(name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _inject_ide_link(source_content, template_name, filename):
|
|
37
|
+
touchable_templates_ide = _get_setting_or_env("TOUCHABLE_TEMPLATES_IDE")
|
|
38
|
+
custom_uri = IDE_TO_URI_MAPPER.get(touchable_templates_ide)
|
|
39
|
+
touchable_templates_root = _get_setting_or_env("TOUCHABLE_TEMPLATES_ROOT") or ""
|
|
40
|
+
touchable_templates_remove_prefix = _get_setting_or_env("TOUCHABLE_TEMPLATES_REMOVE_PREFIX") or ""
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# compute a relative path portion if a remove-prefix was provided
|
|
44
|
+
rel_path = filename
|
|
45
|
+
if touchable_templates_remove_prefix and filename.startswith(touchable_templates_remove_prefix):
|
|
46
|
+
rel_path = filename[len(touchable_templates_remove_prefix):]
|
|
47
|
+
|
|
48
|
+
absolute_template_path = (custom_uri or "") + touchable_templates_root + rel_path
|
|
49
|
+
except Exception:
|
|
50
|
+
absolute_template_path = filename
|
|
51
|
+
|
|
52
|
+
# If template extends another template, we avoid wrapping the child; Django
|
|
53
|
+
# templates are compiled in processor order so this is a best-effort approach.
|
|
54
|
+
|
|
55
|
+
bordered = (
|
|
56
|
+
f'<div class="touchable-templates-container" '
|
|
57
|
+
f'data-template-name="{template_name}" '
|
|
58
|
+
f'data-template-path="{absolute_template_path}" '
|
|
59
|
+
f'style="display: block;">{source_content}</div>'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return bordered
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TouchableTemplatesLoader(FileSystemLoader):
|
|
66
|
+
"""A drop-in replacement for Django's filesystem.Loader that wraps
|
|
67
|
+
template source in a touchable-templates container when enabled.
|
|
68
|
+
|
|
69
|
+
To use: in `TEMPLATES[...]['OPTIONS']['loaders']` reference
|
|
70
|
+
`touchable_templates.django.loader.TouchableTemplatesLoader` instead of
|
|
71
|
+
`django.template.loaders.filesystem.Loader`.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def get_contents(self, origin):
|
|
75
|
+
source = super().get_contents(origin)
|
|
76
|
+
|
|
77
|
+
enabled = _get_setting_or_env("TOUCHABLE_TEMPLATES_ENABLE")
|
|
78
|
+
if not enabled:
|
|
79
|
+
return source
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
template_name = getattr(origin, "template_name", None) or getattr(origin, "name", "")
|
|
83
|
+
filename = getattr(origin, "name", "")
|
|
84
|
+
return _inject_ide_link(source, template_name, filename)
|
|
85
|
+
except Exception:
|
|
86
|
+
logger.exception("touchable_templates: failed to inject IDE link into Django template")
|
|
87
|
+
return source
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TouchableTemplatesAppDirectoriesLoader(AppDirectoriesLoader):
|
|
91
|
+
"""A drop-in replacement for Django's app_directories.Loader that wraps
|
|
92
|
+
template source in a touchable-templates container when enabled.
|
|
93
|
+
|
|
94
|
+
To use: in `TEMPLATES[...]['OPTIONS']['loaders']` reference
|
|
95
|
+
`touchable_templates.django.loader.TouchableTemplatesAppDirectoriesLoader` instead of
|
|
96
|
+
`django.template.loaders.app_directories.Loader`.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def get_contents(self, origin):
|
|
100
|
+
source = super().get_contents(origin)
|
|
101
|
+
|
|
102
|
+
enabled = _get_setting_or_env("TOUCHABLE_TEMPLATES_ENABLE")
|
|
103
|
+
if not enabled:
|
|
104
|
+
return source
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
template_name = getattr(origin, "template_name", None) or getattr(origin, "name", "")
|
|
108
|
+
filename = getattr(origin, "name", "")
|
|
109
|
+
return _inject_ide_link(source, template_name, filename)
|
|
110
|
+
except Exception:
|
|
111
|
+
logger.exception("touchable_templates: failed to inject IDE link into Django app template")
|
|
112
|
+
return source
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TouchableTemplatesMiddleware(MiddlewareMixin):
|
|
7
|
+
"""
|
|
8
|
+
Middleware to post-process Django template responses and mark children with
|
|
9
|
+
the `touchable-templates` class so the client script can provide IDE links.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def process_response(self, request, response):
|
|
13
|
+
if not getattr(settings, "TOUCHABLE_TEMPLATES_ENABLE", False):
|
|
14
|
+
return response
|
|
15
|
+
|
|
16
|
+
content_type = response.get('Content-Type', '').split(';')[0]
|
|
17
|
+
if content_type != 'text/html':
|
|
18
|
+
return response
|
|
19
|
+
|
|
20
|
+
# Decode the response content
|
|
21
|
+
content = response.content.decode(response.charset)
|
|
22
|
+
soup = BeautifulSoup(content, 'html.parser')
|
|
23
|
+
|
|
24
|
+
# Find all elements with class 'touchable-templates-container'
|
|
25
|
+
containers = soup.find_all(class_='touchable-templates-container')
|
|
26
|
+
for container in containers:
|
|
27
|
+
# Get data attributes from the container
|
|
28
|
+
data_template_name = container.get('data-template-name')
|
|
29
|
+
data_template_path = container.get('data-template-path')
|
|
30
|
+
|
|
31
|
+
# Iterate over direct children of the container
|
|
32
|
+
for child in container.find_all(recursive=False):
|
|
33
|
+
# Add 'touchable-templates' class to the child
|
|
34
|
+
existing_classes = child.get('class', [])
|
|
35
|
+
if 'touchable-templates' not in existing_classes:
|
|
36
|
+
child['class'] = existing_classes + ['touchable-templates']
|
|
37
|
+
|
|
38
|
+
# Copy data attributes to the child
|
|
39
|
+
if data_template_name:
|
|
40
|
+
child['data-template-name'] = data_template_name
|
|
41
|
+
if data_template_path:
|
|
42
|
+
child['data-template-path'] = data_template_path
|
|
43
|
+
|
|
44
|
+
# Remove the container but keep its children
|
|
45
|
+
container.unwrap()
|
|
46
|
+
|
|
47
|
+
# Update the response content
|
|
48
|
+
updated_content = str(soup).encode(response.charset)
|
|
49
|
+
response.content = updated_content
|
|
50
|
+
response['Content-Length'] = len(response.content)
|
|
51
|
+
|
|
52
|
+
return response
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
let altToggled = false;
|
|
2
|
+
|
|
3
|
+
// Append the element to the DOM, for example, to the body
|
|
4
|
+
var elemDiv = document.createElement('div');
|
|
5
|
+
elemDiv.id = 'floating-template-name';
|
|
6
|
+
elemDiv.style.cssText = "display: none; position: absolute; background: white; border: 1px solid black; padding: 5px; z-index: 9999;"
|
|
7
|
+
document.body.appendChild(elemDiv);
|
|
8
|
+
const floatingElement = document.getElementById("floating-template-name");
|
|
9
|
+
|
|
10
|
+
document.addEventListener("DOMContentLoaded", function(event) {
|
|
11
|
+
|
|
12
|
+
document.addEventListener('keydown', function(event) {
|
|
13
|
+
if (event.key === 'Alt') {
|
|
14
|
+
altToggled = !altToggled;
|
|
15
|
+
if (!altToggled) {
|
|
16
|
+
floatingElement.style.display = 'none';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
document.addEventListener('click', function(event) {
|
|
22
|
+
if (altToggled) {
|
|
23
|
+
let closestElement = event.target.closest('.touchable-templates');
|
|
24
|
+
if (closestElement) {
|
|
25
|
+
let templatePath = closestElement.getAttribute('data-template-path');
|
|
26
|
+
if (templatePath) {
|
|
27
|
+
window.open(templatePath, '_blank');
|
|
28
|
+
event.stopPropagation();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, true);
|
|
33
|
+
|
|
34
|
+
document.addEventListener('mousemove', (e) => {
|
|
35
|
+
if (!altToggled) return;
|
|
36
|
+
const el = e.target.closest('.touchable-templates');
|
|
37
|
+
if (!el) return floatingElement.style.display = 'none';
|
|
38
|
+
|
|
39
|
+
const name = el.getAttribute('data-template-name');
|
|
40
|
+
if (!name) return;
|
|
41
|
+
|
|
42
|
+
floatingElement.textContent = name;
|
|
43
|
+
floatingElement.style.display = 'block';
|
|
44
|
+
const { pageX: x, pageY: y } = e;
|
|
45
|
+
const { offsetWidth: w, offsetHeight: h } = floatingElement;
|
|
46
|
+
const { innerWidth: vw, innerHeight: vh } = window;
|
|
47
|
+
floatingElement.style.left = `${x + w + 20 > vw ? x - w - 5 : x + 10}px`;
|
|
48
|
+
floatingElement.style.top = `${y + h + 20 > vh ? y - h - 5 : y + 10}px`;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
document.addEventListener('mouseleave', () => {
|
|
52
|
+
floatingElement.style.display = 'none';
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
document.addEventListener('mouseenter', (e) => {
|
|
56
|
+
if (altToggled) {
|
|
57
|
+
const el = e.target.closest('.touchable-templates');
|
|
58
|
+
if (el) {
|
|
59
|
+
const name = el.getAttribute('data-template-name');
|
|
60
|
+
floatingElement.textContent = name;
|
|
61
|
+
floatingElement.style.display = 'block';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 2
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "beautifulsoup4"
|
|
7
|
+
version = "4.14.3"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "soupsieve" },
|
|
11
|
+
{ name = "typing-extensions" },
|
|
12
|
+
]
|
|
13
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
|
14
|
+
wheels = [
|
|
15
|
+
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[[package]]
|
|
19
|
+
name = "soupsieve"
|
|
20
|
+
version = "2.8.3"
|
|
21
|
+
source = { registry = "https://pypi.org/simple" }
|
|
22
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
|
|
23
|
+
wheels = [
|
|
24
|
+
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[[package]]
|
|
28
|
+
name = "touchable-templates"
|
|
29
|
+
version = "2026.2.4a0"
|
|
30
|
+
source = { editable = "." }
|
|
31
|
+
dependencies = [
|
|
32
|
+
{ name = "beautifulsoup4" },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[package.metadata]
|
|
36
|
+
requires-dist = [{ name = "beautifulsoup4", specifier = ">=4.9.2" }]
|
|
37
|
+
|
|
38
|
+
[[package]]
|
|
39
|
+
name = "typing-extensions"
|
|
40
|
+
version = "4.15.0"
|
|
41
|
+
source = { registry = "https://pypi.org/simple" }
|
|
42
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
43
|
+
wheels = [
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
45
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|