Compare commits
5
Commits
73f68e9a2f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e227a0802 | ||
|
|
4604f6923a | ||
|
|
73b9ae4dd6 | ||
|
|
06a4ea6870 | ||
|
|
1cc070b7c0 |
@@ -5,6 +5,34 @@
|
||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
|
||||
и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/).
|
||||
|
||||
## [0.2.9] - 2026-10-07
|
||||
|
||||
### Добавлено
|
||||
- Оптимизация SEO и поисковой семантики: добавлены целевые мета-теги (`title`, `description`, `keywords`, `og:title`, `og:description`) с релевантными ключевыми запросами для веб-типографики.
|
||||
- Индексация главной страницы (`/`) в `sitemap.xml` через добавление `StaticViewSitemap` с приоритетом 1.0.
|
||||
- Развернутый справочно-информационный раздел на главной странице:
|
||||
- 9 карточек возможностей типографа (неразрывные пробелы с обработкой сквозь скобки и кавычки, кавычки-ёлочки и лапки, длинные/средние тире и числовые интервалы с римскими/арабскими числами, инициалы и сокращения организаций/титулов, висячая пунктуация, HTML-санитайзер и спецсимволы, мягкие переносы `­`, форматы вывода Unicode/HTML/Mixed, бесплатный и безлимитный доступ без сохранения текстов на сервере).
|
||||
- Секция целевой аудитории (копирайтеры, верстальщики, контент-менеджеры, дизайнеры).
|
||||
- Блок интеграции открытой Python-библиотеки `etpgrf` со сниппетами установки и использования.
|
||||
- Справочный аккордеон с ответами на частые вопросы (FAQ).
|
||||
- Сворачивание информационного SEO-блока в компактный спойлер (Bootstrap Collapse) для сохранения чистоты и минимализма главного экрана сервиса.
|
||||
- Интерактивный hover-эффект для карточек возможностей с адаптивной подсветкой (светлая тень в тёмной теме оформления и глубокая тень в светлой теме).
|
||||
- Автоматический тест валидации метаданных SEO и наличия корня сайта в карте сайта `sitemap.xml`.
|
||||
|
||||
### Исправлено
|
||||
- Формирование канонических URL (`rel="canonical"`): очистка от случайных query-параметров и меток сессий.
|
||||
|
||||
## [0.2.8] - 2026-10-04
|
||||
|
||||
### Добавлено
|
||||
- Локальная статика для внешних фронтенд-библиотек (Bootstrap 5, Bootstrap Icons, HTMX, Alpine.js, CodeMirror 6) для работы интерфейса при недоступности CDN.
|
||||
- Стратегия fallback-подключения в шаблонах: автоматическое переключение на локальную статику при сбое или блокировке CDN.
|
||||
- Скрипт копирования production-ассетов `frontend-assembly/copy-vendor.js` и обновление сборки через `frontend-assembly/assembly_via_npm.sh`.
|
||||
- Автоматические тесты проверки наличия статических вендорных ассетов и корректности рендеринга fallback-подключений в `typograph/tests.py`.
|
||||
|
||||
### Изменено
|
||||
- Обновлен и структурирован `.gitignore` с подробными комментариями.
|
||||
|
||||
## [0.2.7] - 2025-03-22
|
||||
### Изменено
|
||||
- Обновление всех пактов, включая `etpgrf` (v0.1.5 → v0.1.6.post1).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Основные возможности:
|
||||
- Веб-интерфейс для ввода текста и настройки параметров типографики.
|
||||
"""
|
||||
__version__ = "0.2.7"
|
||||
__version__ = "0.2.9"
|
||||
__author__ = "Sergei Erjemin"
|
||||
__email__ = "erjemin@gmail.com"
|
||||
__license__ = "MIT"
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
from .models import Post
|
||||
|
||||
class StaticViewSitemap(Sitemap):
|
||||
"""Карта сайта для статических страниц (главная и др.)."""
|
||||
priority = 1.0
|
||||
changefreq = "daily"
|
||||
|
||||
def items(self):
|
||||
return ['index']
|
||||
|
||||
def location(self, item):
|
||||
return reverse(item)
|
||||
|
||||
class PostSitemap(Sitemap):
|
||||
changefreq = "weekly" # Как часто меняются страницы
|
||||
priority = 0.9 # Приоритет (от 0.0 до 1.0)
|
||||
|
||||
@@ -4,175 +4,314 @@
|
||||
{% block title %}Песочница верстки — ETPGRF{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
|
||||
{# Левая колонка: Дата и Картинка #}
|
||||
<div class="col-lg-2 align-self-start text-end mb-4">
|
||||
<p class="small align-self-end">
|
||||
<small class="bg-secondary bg-opacity-10 p-2 text-nowrap">
|
||||
12.фев.2026
|
||||
</small>
|
||||
</p>
|
||||
<p>
|
||||
<img src="{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}" class="w-100" alt="Django Admin" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="row align-items-start">
|
||||
<div class="col border-end">
|
||||
<h3>Сайт typograph.cube2.ru</h3>
|
||||
|
||||
{# Правая колонка: Контент #}
|
||||
<div class="col-lg-10 border-start ps-lg-4 post-page-content">
|
||||
<p>Исходный код сайта онланй-типографа <tt>etpgrf</tt> доступен в нескольких репози­ториях (<a
|
||||
href="https://github.com/erjemin/etpgrf-site" target="_blank">GitHub</a>, <a
|
||||
href="https://gitverse.ru/erjemin/etpgrf-site" target="_blank">GitVerse</a> и <a
|
||||
href="https://git.cube2.ru/erjemin/2026-etpgrf-site" target="_blank">Сube2</a>. Наиболее заметные изменения
|
||||
сайта онлайн-типографа приводятся ниже в хронологическом порядке. Для полного списка изменений,
|
||||
включая мелкие фиксы и коммиты, пожалуйста, обратитесь к истории репозитория.</p>
|
||||
|
||||
<h1>Массовая типографика контента в Django: пользова­тельские команды управления (Custom Management Commands)</h1>
|
||||
|
||||
<div class="lead bg-secondary bg-opacity-10 p-3 rounded">
|
||||
|
||||
<p>Если вы работали с Django, вы наверняка использовали команду <code>manage.py</code> сотни раз: <code>python manage.py runserver</code>, <code>migrate</code>, <code>createsuperuser</code>.</p>
|
||||
<p>Но знаете ли вы, что можете легко создавать <strong>свои собственные</strong> команды? Это один из самых мощных и недоо­цененных инструментов Django для автома­тизации рутины. В этом посте я расскажу, что такое <strong>Management Command</strong>, зачем она нужна и как её использовать на примере реальной задачи: массовой типографики контента в базе данных.</p>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
<div class="post-content mt-4">
|
||||
|
||||
<h2>Что это такое?</h2>
|
||||
<p><strong>Management Command</strong> — это обычный Python-скрипт, который «встраивается» в экосистему Django. Он имеет доступ к моделям, настройкам (settings.py) и базе данных, но запускается из консоли через <code>python manage.py ...</code>.</p>
|
||||
<h2>Зачем это нужно?</h2>
|
||||
<p>Представьте ситуации:</p>
|
||||
<h4>[0.2.8] - 2026–10–04</h4>
|
||||
<h5>Добавлено</h5>
|
||||
<ul>
|
||||
<li>Нужно пересчитать рейтинг для десятки тысяч товаров на сайте или сделать типографику всех публикаций после подключения etpgrf-типографа (или любого другого).</li>
|
||||
<li>Необходимо обновить цены для всех товарных карточек в соответствии с таблицей excel или по API (или спарсить данные с внешнего сайта).</li>
|
||||
<li>Нужно отправить e-mail рассылку пользо­вателям.</li>
|
||||
<li>Нужно пометить комментарии или публикации как архивные в соответствии с правилом или почистить базу от старых логов.</li>
|
||||
<li>Локальная статика для внешних фронтенд-библиотек (Bootstrap 5, Bootstrap Icons, HTMX, Alpine.js,
|
||||
CodeMirror 6) для работы интер­фейса при недос­тупности CDN.
|
||||
</li>
|
||||
<li>Стратегия fallback-подключения в шаблонах: автома­тическое переклю­чение на локальную
|
||||
статику при сбое или блокировке CDN.
|
||||
</li>
|
||||
<li>Скрипт копиро­вания production-ассетов <code>frontend-assembly/copy-vendor.js</code>
|
||||
и обновление сборки через <code>frontend-assembly/assembly_via_npm.sh</code>.
|
||||
</li>
|
||||
<li>Автомати­ческие тесты проверки наличия стати­ческих вендорных ассетов и коррект­ности
|
||||
ренде­ринга fallback-подключений в <code>typograph/tests.py</code>.
|
||||
</li>
|
||||
</ul>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Обновлен и структу­рирован <code>.gitignore</code> с подроб­ными коммен­тариями.</li>
|
||||
</ul>
|
||||
<p>Делать это через <tt>views.py</tt> (views) — плохая идея (страница может отвалиться по тайм-ауту). Писать отделный скрипт рядом <tt>manage.py</tt> — неудобно (нужно вручную настраивать `DJANGO_SETTINGS_MODULE`). Встроенные команды решают эти проблемы элегантно.</p>
|
||||
<h2>Как это работает?</h2>
|
||||
<p>Django использует систему «автообна­ружения» (auto-discovery). Чтобы ваша команда появилась в списке `manage.py`, нужно соблюсти строгую иерархию папок внутри вашего приложения (например, <tt>web</tt>):</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">web/
|
||||
├── __init__.py
|
||||
├── models.py
|
||||
└── management/ <-- 1. Создаем папку management
|
||||
├── __init__.py
|
||||
└── nginx/
|
||||
├── commands/ <-- 2. Внутри неё папку commands
|
||||
├── __init__.py
|
||||
└── my_cool_script.py <-- 3. Наш файл с командой</pre>
|
||||
<p>Файл <tt>my_cool_script.py</tt> автома­тически превратится в команду: <code>python manage.py my_cool_script</code>. Django будет искать его в папках <tt>management/commands/</tt> внутри каждого устано­вленного приложения. Это позволяет легко организовать код и держать все «команды обслуживания» в одном месте.</p>
|
||||
<p><strong>Для справки:</strong> Механизм Custom Management Commands появился еще в Django 0.96 (в глубокой древности) и с тех пор является стандартом де-факто для написания скриптов обслуживания.</p>
|
||||
<h2>Анатомия команды</h2>
|
||||
<p>Вот пример простейшей команды. Мы наследуемся от класса <code>BaseCommand</code> и переопре­деляем метод <code>handle</code>:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
# web/management/commands/hello.py
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Выводит приветствие' # Описание для --help
|
||||
<h4>[0.2.7] - 2026–03–22</h4>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Обновление всех пактов, включая <tt>etpgrf</tt> (v0.1.5 → v0.1.6.post1).</li>
|
||||
</ul>
|
||||
<h4>Исправлено</h4>
|
||||
<ul>
|
||||
<li><code>canonical</code> для страниц учитывает https/http.</li>
|
||||
</ul>
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Можно добавлять аргументы, как в argparse
|
||||
parser.add_argument('name', type=str, help='Имя пользователя')
|
||||
<h4>[0.2.6] - 2026–02–28</h4>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Подключена новая версия библиотеки <tt>etpgrf</tt> (v0.1.4 → v0.1.5).</li>
|
||||
</ul>
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Главная логика
|
||||
name = options['name']
|
||||
self.stdout.write(f"Привет, {name}!")</pre>
|
||||
<p>Запуск:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
python manage.py hello Иван</pre>
|
||||
<p>Увидим в консоли:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
Привет, Иван!</pre>
|
||||
<h2>Реальный пример: Массовая типографика через etpgrf</h2>
|
||||
<p>В моём проекте, <a href="https://dq.cube2.ru/" target="_blank">DQ – коллекция цитат: место для вдумчивого чтения</a>, контент был частично обработан с помощью Типографа Муравьёа, частично вручную. И вот я установил и настроил etpgrf (тем более что Типограф Муравьёва «почил в бозе», да и до того не обновлялся с 2018 года). Все новые цитаты типогра­фируются через etpgrf, но что делать со старыми? Их сотни, и открывать и «пересо­хранаять» каждую вручную — это адский труд. Вот тут на помощь и приходит Custom Management Command.</p>
|
||||
<p>Вот как я это реализовал. В <tt>web/management/commands/reprocess_typography.py</tt>:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
from django.core.management.base import BaseCommand
|
||||
from web.models import TbDictumAndQuotes
|
||||
# Импорты библиотеки etpgrf
|
||||
from etpgrf.typograph import Typographer
|
||||
from etpgrf.sanitizer import SanitizerProcessor
|
||||
# ... другие процессоры, если нужно ...
|
||||
<h4>[0.2.5] — 2026–02–13</h4>
|
||||
<h5>Добавлено</h5>
|
||||
<ul>
|
||||
<li>Редизайн списка постов в блоге: шахматный порядок, вертикальные разделители, улучшенная адаптивность
|
||||
для мобильных
|
||||
устройств.
|
||||
</li>
|
||||
<li>Поле <code>updated_at</code> (<em>Дата обновления</em>) в модели, админке, блогах, страницах, <tt>sitemaps.xml</tt>
|
||||
и микро­разметке <tt>Schema.org</tt> для улучшения <abbr
|
||||
title="Search Engine Optimization / Поисковая оптимизация">SEO</abbr>, <abbr
|
||||
title="Generative Engine Optimization / Оптимизацию для Генеративных нейросетей и ИИ-поисковиков">GEO</abbr>
|
||||
и <abbr title="Large Language Model Optimization / Оптимизация под Большие Языковые Модели">LLMO</abbr>.
|
||||
</li>
|
||||
<li><tt>README.md</tt> с описанием проекта онлайн-типографа, его особенностей, технического стека и инструкциями
|
||||
по установке и запуску.
|
||||
</li>
|
||||
<li>Отображение заголовков постов в списке админки без HTML-мнемоник (декоди­рование <code>&nbsp;</code>
|
||||
и др.).
|
||||
</li>
|
||||
</ul>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Исправлены ошибки в шаблоне <tt>post_list.html</tt> (и полностью переработан дизайн в целом).
|
||||
<ul>
|
||||
<li>Улучшено отображение даты и скрытие декоративных изображений в списке постов на мобильных
|
||||
устройствах.
|
||||
</li>
|
||||
<li>Оптими­зированы отступы и типографика в списке постов.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Формирование <code>slug</code> из <code>title</code> при сохранении поста или страницы с исполь­зованием
|
||||
библиотеки <code>pytils</code> для трансли­терации с очистикой от HTML-мнемоник и создания
|
||||
<abbr title="Uniform Resource Locator / Унифицированный Указатель Ресурса">URL</abbr>-дружес­твенных
|
||||
строк.
|
||||
</li>
|
||||
<li>Дизайн и вёрстка страниц для постов блога и вспомо­гательных страниц для мобильных
|
||||
устройств (адаптивность, скрытие картинки-обложки).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Переобрабатывает все цитаты'
|
||||
<h4>[0.2.4] — 2026–02–12</h4>
|
||||
<h5>Добавлено</h5>
|
||||
<ul>
|
||||
<li>Микро­разметка <code>Schema.org</code> (JSON-LD) для постов и страниц для улучшения
|
||||
SEO и понимания контента поисковиками и ИИ.
|
||||
</li>
|
||||
<li>Файл <code>llms.txt</code> для предоста­вления информации о сайте и API для больших
|
||||
языковых моделей (<abbr title="Large Language Model">LLM</abbr>) в соответвиии со стандартом GEO
|
||||
и LLMO.
|
||||
</li>
|
||||
<li>Кастомный фильтр <code>unescape</code> для очистки мета-тегов от HTML-сущностей и переводов
|
||||
строк.
|
||||
</li>
|
||||
</ul>
|
||||
<h5>Исправлено</h5>
|
||||
<ul>
|
||||
<li>Исправлена ошибка, при которой счетчик символов не обновлялся при восста­новлении вкладки
|
||||
из истории браузера.
|
||||
</li>
|
||||
<li>Исправлена ошибка экрани­рования кавычек в <tt>JSON-LD</tt>, <tt>Title</tt>
|
||||
и <tt>Description</tt>.
|
||||
</li>
|
||||
<li>В <code>docker-compose.yaml</code> додавлен перезапуск <code>watchtower</code> при его
|
||||
остановке.
|
||||
</li>
|
||||
</ul>
|
||||
<h4>[0.2.3] — 2026–02–11</h4>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Добавлена кнопка очистки текста во вводном поле и счетчик вводимых символов.</li>
|
||||
</ul>
|
||||
<h4>[0.2.2] — 2026–02–03</h4>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>В онлайн-типографе подключена новая версия библиотеки <tt>etpgrf</tt> (v0.1.3 → v0.1.4).</li>
|
||||
<li>Незначи­тельные улучшения в оформлении.</li>
|
||||
</ul>
|
||||
<h4>v0.2.1 — 2026–01–30</h4>
|
||||
<h5>Исправлено</h5>
|
||||
<ul>
|
||||
<li>Исправление ошибок при формировании мета-тегов, картинок, <code>alt</code> под картинками
|
||||
и т. п.
|
||||
</li>
|
||||
<li>Исправлена ошибка в настройках nginx внутри docker-контейнера возникавшая при отдаче
|
||||
media-файлов.
|
||||
</li>
|
||||
</ul>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>При создании записи в блог или страницы «тизер» обязателен!</li>
|
||||
</ul>
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Добавляем флаг "сухой прогон"
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Запустить без сохранения изменений',
|
||||
)
|
||||
# Ограничение количества
|
||||
parser.add_argument('--limit', type=int, help='Сколько записей обработать')
|
||||
# Смещение (для пагинации)
|
||||
parser.add_argument('--offset', type=int, default=0, help='Сколько пропустить')
|
||||
<h4>v0.2.0 — 2026–01–28</h4>
|
||||
<h5>Добавлено</h5>
|
||||
<ul>
|
||||
<li>Приложение blog (для страниц и постов) и соответ­ствующие изменения в моделях базы,
|
||||
добавление новых view и шаблонов.
|
||||
</li>
|
||||
<li>Песочница (шаблон <code>blog/templates/blog/tmp.html</code> для тестирования верстки (доступен только в режиме
|
||||
debug).
|
||||
</li>
|
||||
<li>Динамическое создание sitemap.xml</li>
|
||||
<li>robots.txt</li>
|
||||
<li>Изменения в шапке сайта (меню и бургер)</li>
|
||||
</ul>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Спрятан <code>url</code> админки типографа. Его расположение теперь задается через переменные окружения
|
||||
в <code>.env</code>.
|
||||
</li>
|
||||
<li><code>favicon.ico</code> оптими­зирована для Яндекс (120х120px).</li>
|
||||
<li>Исправлено поведение шапки и логотива для мобильных устройств.</li>
|
||||
</ul>
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# 1. Настраиваем типограф
|
||||
# ... инициализация процессоров ...
|
||||
settings = {
|
||||
# ...
|
||||
'sanitizer': SanitizerProcessor(mode="html"), # Режим HTML-очистки
|
||||
'hanging_punctuation': 'left', # Режим висячей пунктуации
|
||||
# ...
|
||||
}
|
||||
typographer = Typographer(**settings)
|
||||
<h4>v0.1.8 — 2026–01–23</h4>
|
||||
Коммит:
|
||||
846c066
|
||||
|
||||
# 2. Получаем записи из базы с учетом limit/offset
|
||||
qs = TbDictumAndQuotes.objects.all().order_by('id')
|
||||
<h4>v0.1.7 — 2026–01–23</h4>
|
||||
Коммит:
|
||||
d74bee2
|
||||
|
||||
start = options['offset']
|
||||
if options['limit']:
|
||||
qs = qs[start : start + options['limit']]
|
||||
else:
|
||||
qs = qs[start:]
|
||||
<h4>v0.1.6 — 2026–01–23</h4>
|
||||
Коммит:
|
||||
6b4dbaf
|
||||
|
||||
self.stdout.write(f"Найдено {qs.count()} цитат...")
|
||||
<h4>v0.1.5 — 2026–01–21</h4>
|
||||
Коммит:
|
||||
78174a8
|
||||
|
||||
# 3. Бежим по циклу с прогресс-баром (tqdm если есть)
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
iterator = tqdm(qs)
|
||||
except ImportError:
|
||||
iterator = qs
|
||||
<h4>v0.1.4 — 2026–01–20</h4>
|
||||
Коммит:
|
||||
2d09aef
|
||||
|
||||
for dq in iterator:
|
||||
try:
|
||||
# Обрабатываем текст
|
||||
new_html = typographer.process(dq.szContent)
|
||||
<h4>v0.1.3 — 2026–01–19</h4>
|
||||
Коммит:
|
||||
66f2228
|
||||
|
||||
if options['dry_run']:
|
||||
self.stdout.write(f"[{dq.id}] Предпросмотр: {new_html[:50]}...")
|
||||
else:
|
||||
dq.szContentHTML = new_html
|
||||
dq.save(update_fields=['szContentHTML'])
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Ошибка id={dq.id}: {e}"))
|
||||
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"\nГотово!"))</pre>
|
||||
<p>Возможно вы заметили, что в моем проекте для контента есть два поля: <code>szContent</code> и <code>szContentHTML</code>. В первом хранится «сырой» текст, во втором — результат типогра­фирования. В вашем проекте, скорее всего, только одно поле для контента, но по сути это ничего не меняет.</p>
|
||||
<p>Еще интересные фишки, которые исполь­зовались при типогра­фировании:</p>
|
||||
<ol>
|
||||
<li><code>self.stdout.write</code> вместо <code>print</code> – позволяет Django перехва­тывать вывод (например, для тестов) и корректно работать с кодировками.</li>
|
||||
<li><code>self.style.SUCCESS</code> и <code>self.style.ERROR</code> – раскрашивает текст в консоли (зеленый/красный) и это очень удобно для визуального восприятия логов.</li>
|
||||
<li>Аргумент <tt>--dry-run</tt> — позволяют безопасно тестировать скрипт на продакшене перед тем, как реально менять данные.</li>
|
||||
<li>Аргументы <tt>--limit</tt> и <tt>--offset</tt> — позволяют обрабатывать базу «порциями», что полезно для больших объемов данных (можно запустить несколько процессов параллельно с разными <tt>offset</tt>).</li>
|
||||
<li>Используется <code>update_fields</code> — позволяет переза­писывать не всю модель целиком (что могло бы затереть изменения, сделанные кем-то другим в ту же секунду), а обновляем только конкретные поля.</li>
|
||||
</ol>
|
||||
<p>Теперь, чтобы типогра­фировать весь контент, нам достаточно одной строку в терминале, и Django сделает всю грязную работу за нас (не забудьте инициировать виртуальное окружение вашего проекта).</p>
|
||||
<p>Тестовый прогон (безопасно, ничего не сохраняет):</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
python manage.py reprocess_typography --dry-run</pre>
|
||||
<p>Боевой запуск, с изменениями данные в базе:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
python manage.py reprocess_typography</pre>
|
||||
<p>Если у вас очень много записей, можно запускать по частям:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
python manage.py reprocess_typography --limit 1000 --offset 0
|
||||
python manage.py reprocess_typography --limit 1000 --offset 1000
|
||||
python manage.py reprocess_typography --limit 1000 --offset 2000</pre>
|
||||
<h4>v0.1.2 — 2026–01–18</h4>
|
||||
Коммит:
|
||||
92711f5
|
||||
|
||||
<h4>v0.1.1 — 2026–01–16</h4>v0.1.1
|
||||
Коммит:
|
||||
5d5d48d
|
||||
|
||||
<h4>v0.1.0 — 2026–01–16</h4>
|
||||
Коммит:
|
||||
3a7bb29
|
||||
|
||||
</div>
|
||||
<div class="col border-start">
|
||||
<h3>Библиотека etpgrf</h3>
|
||||
<p>
|
||||
Исходный код etpgrf-типографа доступен в нескольких репози­ториях (<a
|
||||
href="https://github.com/erjemin/etpgrf" target="_blank">GitHub</a>, <a
|
||||
href="https://gitverse.ru/erjemin/etpgrf" target="_blank">GitVerse</a>, <a
|
||||
href="https://git.cube2.ru/erjemin/2025-etpgrf" target="_blank">Сube2</a>, и <a
|
||||
href="https://pypi.org/project/etpgrf/" target="_blank">PyPI</a>), распрос­траняется под лицензией <a
|
||||
href="https://opensource.org/licenses/MIT" target="_blank">MIT</a>, может быть установлен локально, на ваш
|
||||
сайт или интегри­рован в ваши проекты как Python-библиотека.</p>
|
||||
|
||||
<h4>[0.1.6.post1] - 2026–03–19</h4>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Новый алгоритм «висячей пунктуации» (<em>HangingPunctuationProcessor</em>). Добавлены компенсирующие
|
||||
пробелы для висячих символов, чтобы избежать наложения на соседние слова. Теперь «висячие символы» (кавычки,
|
||||
скобки и т. п.) оборачиваются в <code><span></code> вместе с ближайшим словом и пробелом, что
|
||||
обеспечивает отсутствие визуальных смещений внутри строки. Режим <code>both</code> (для одновременного
|
||||
вывешивания в обе стороны) отключен из-за потенциальных конфликтов компенсирующих пробелов и проблем
|
||||
с выравни­ванием при исполь­зовании CSS <code>text-justify</code>.</li>
|
||||
</ul>
|
||||
<h4>Исправлено</h4>
|
||||
<ul>
|
||||
<li>Автоматическая замена символов <code>x</code>, <code>X</code>, <code>х</code>, <code>Х</code>, стоящих между
|
||||
числами, на знак умножения <code>×</code>, чтобы выражения вида <code>100x100</code> или <code>100 х 100</code>
|
||||
корректно обрабатывались и выглядели типографски правильными (<code>100×100</code> или <code>100&nbsp;×&nbsp;100</code>).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>[0.1.5] - 2026–02–27</h4>
|
||||
<h5>Исправлено</h5>
|
||||
<ul>
|
||||
<li>Исправлена ошибка, из-за которой <code>&amp;</code> в исходном тексте некорректно преобразовывался
|
||||
в <code>&</code>. Теперь <code>&amp;</code>; и его маскирование (<code>&amp;amp;</code>,
|
||||
<code>&amp;lt;</code> и т. д.) сохраняются в итоговом HTML.
|
||||
</li>
|
||||
<li>Исправлена (частично) расстановка неразрывных пробелов <code>&nbsp;</code> на границах закрывающих
|
||||
тегов (например, <code><b>Текст</b> — слово</code> теперь корректно обрабатывается, в <code><b>Текст</b>&nbsp;—
|
||||
слово)</code>.
|
||||
</li>
|
||||
</ul>
|
||||
<h4>[0.1.4] - 2026–02–03</h4>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li><b>Архите­ктурное улучшение:</b> Полностью переработан механизм обработки HTML.</li>
|
||||
<li>Внедрены <b>маркеры границ узлов</b> (<code>\uFFFF</code>) при сборке текста. Это позволяет корректно
|
||||
восста­навливать структуру HTML даже если длина текста изменилась в процессе обработки (например, при удалении лишних
|
||||
пробелов).
|
||||
</li>
|
||||
<li>Внедрены <b>плейсхолдеры</b> (<code>\uFFFC</code>) для защищенных тегов (<code><code></code>,
|
||||
<code><script></code> и др.). Теперь содержимое этих тегов физически изолируется перед обработкой,
|
||||
что предот­вращает «протекание» контекста (например, склеивание слов, разделенных кодом).
|
||||
</li>
|
||||
</ul>
|
||||
<h5>Исправлено</h5>
|
||||
<ul>
|
||||
<li>Исправлена ошибка смещения текста при наличии спецсимволов (мнемоник) или при изменении длины
|
||||
строки.
|
||||
</li>
|
||||
<li>Исправлена обработка кавычек, стоящих вплотную к границам тегов (например, <code>"<b>Текст</b>"</code>).
|
||||
</li>
|
||||
</ul>
|
||||
<h4>v0.1.3 — 2026–01–11</h4>
|
||||
<h5>Исправлено</h5>
|
||||
<ul>
|
||||
<li>Исправлена проблема с появлением лишних тегов <code><html></code>
|
||||
и <code><body></code> при обработке фрагментов HTML (когда исполь­зуется парсер
|
||||
<code>lxml</code>). Теперь типограф автома­тически определяет, был ли на входе полноценный
|
||||
документ или фрагмент, и возвращает соответ­ствующий результат.
|
||||
</li>
|
||||
</ul>
|
||||
<h4>v0.1.2 — 2025–12–27</h4>
|
||||
<h5>Исправлено</h5>
|
||||
<ul>
|
||||
<li><strong>Критическое исправление:</strong> Добавлена отсут­ствующая зависимость <code>regex</code> в <code>pyproject.toml</code>.
|
||||
Без неё библиотека падала при импорте.
|
||||
</li>
|
||||
</ul>
|
||||
<h4>v0.1.1 — 2025–12–23</h4>
|
||||
<h5>Добавлено</h5>
|
||||
<ul>
|
||||
<li>Ссылки на зеркала репозитория (GitVerse, Gitea) в <code>pyproject.toml</code> и <code>README.md</code>.
|
||||
</li>
|
||||
<li>Раздел Credits в документации.</li>
|
||||
</ul>
|
||||
<h4>v0.1.0 — 2025–12–23</h4>
|
||||
<h5>Добавлено</h5>
|
||||
<ul>
|
||||
<li>Первый публичный релиз библиотеки <code>etpgrf</code>.</li>
|
||||
<li>
|
||||
Основные модули:
|
||||
<ul>
|
||||
<li><code>Typographer</code>: основной класс-оркестратор.</li>
|
||||
<li><code>Hyphenator</code>: расстановка мягких переносов (алгоритм Ляна-Кнута).</li>
|
||||
<li><code>QuotesProcessor</code>: замена кавычек («ёлочки», „лапки“).</li>
|
||||
<li><code>Unbreakables</code>: неразрывные пробелы для предлогов, союзов и частиц.</li>
|
||||
<li><code>LayoutProcessor</code>: типографика тире, инициалов, акронимов, единиц измерения, устойчивых
|
||||
сокращений (постпо­зиционных и препози­ционных).</li>
|
||||
<li><code>SymbolsProcessor</code>: псевдо­графика (тире, стрелочки, копирайт и т. п.)</li>
|
||||
<li><code>HangingPunctuationProcessor</code>: висячая пунктуация.</li>
|
||||
<li><code>SanitizerProcessor</code>: очистка HTML перед обработкой.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Поддержка русского, русского дорефо­рменного и английского языков.</li>
|
||||
<li>Поддержка обработки HTML (через BeautifulSoup).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,10 +4,11 @@ from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.sitemaps.views import sitemap # Импортируем view для sitemap
|
||||
from blog import views as blog_views
|
||||
from blog.sitemaps import PostSitemap # Импортируем наш класс Sitemap
|
||||
from blog.sitemaps import PostSitemap, StaticViewSitemap # Импортируем классы Sitemap
|
||||
|
||||
# Словарь с картами сайта
|
||||
sitemaps = {
|
||||
'static': StaticViewSitemap,
|
||||
'posts': PostSitemap,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,25 +3,25 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{# SEO & Meta Tags #}<title>{% block title %}ETPGRF — единая типографика для веба{% endblock %}</title>
|
||||
<meta name="description" content="{% block description %}Бесплатный онлайн-типограф для подготовки текстов к публикации в вебе. Расставка неразрывных пробелов, правильных кавычек («ёлочки»), тире, спецсимволы, отбивка, компоновка, висячая пунктуация. Идеально для верстки сайтов, статей и постов.{% endblock %}">
|
||||
<meta name="keywords" content="{% block keywords %}типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев{% endblock %}">
|
||||
{# SEO & Meta Tags #}<title>{% block title %}Онлайн-типограф для текста и HTML — ETPGRF{% endblock %}</title>
|
||||
<meta name="description" content="{% block description %}Онлайн-типограф для подготовки HTML и публикаций на русском и английском: кавычки «ёлочки» и “лапки”, длинное тире, неразрывные пробелы (&nbsp;), отбивка предлогов, артиклей и чисел, висячая пунктуация…{% endblock %}">
|
||||
<meta name="keywords" content="{% block keywords %}онлайн типограф, типографика онлайн, типограф для текста, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, подготовка текста для веба, висячая пунктуация, python etpgrf, типограф лебедева муравьева{% endblock %}">
|
||||
<meta name="author" content="Sergei Erjemin">
|
||||
{# Schema.org (JSON-LD) #}{% block schema %}{% endblock %}
|
||||
{# Open Graph (Facebook, VK, LinkedIn, Telegram) #}<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="ETPGRF" />
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}" />
|
||||
<meta property="og:title" content="{% block og_title %}ETPGRF — единая типографика для веба{% endblock %}" />
|
||||
<meta property="og:description" content="{% block og_description %}Сделайте ваш текст профессиональным и готовым к публикации в интернете за один клик. Умная типографика для веб-дизайнеров, редакторов и контент-менеджеров.{% endblock %}" />
|
||||
<meta property="og:url" content="{% block og_url %}{{ request.scheme }}://{{ request.get_host }}{{ request.path }}{% endblock %}" />
|
||||
<meta property="og:title" content="{% block og_title %}Онлайн-типограф для текста и HTML — ETPGRF{% endblock %}" />
|
||||
<meta property="og:description" content="{% block og_description %}Бесплатный онлайн-типограф для подготовки текстов и HTML к публикации: кавычки «ёлочки», неразрывные пробелы, длинное тире и висячая пунктуация.{% endblock %}" />
|
||||
<meta property="og:image" content="{% block og_image %}{{ request.scheme }}://{{ request.get_host }}{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}{% endblock %}" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
{# --- Twitter Cards (X) --- #}<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{% block twitter_title %}ETPGRF — единая типографика для веба{% endblock %}" />
|
||||
<meta name="twitter:description" content="{% block twitter_description %}Сделайте ваш текст профессиональным и готовым к публикации в интернете за один клик.{% endblock %}" />
|
||||
<meta name="twitter:title" content="{% block twitter_title %}Онлайн-типограф для текста и HTML — ETPGRF{% endblock %}" />
|
||||
<meta name="twitter:description" content="{% block twitter_description %}Бесплатный онлайн-типограф для подготовки текстов и HTML к публикации в вебе.{% endblock %}" />
|
||||
<meta name="twitter:image" content="{% block twitter_image %}{{ request.scheme }}://{{ request.get_host }}{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}{% endblock %}" />
|
||||
{# Шутка #}<meta name="generator" content="Microsoft FrontPage 1.0"/>
|
||||
{# Canonical #}<link rel="canonical" href="{{ request.build_absolute_uri }}"/>
|
||||
{# Canonical #}<link rel="canonical" href="{% block canonical %}{{ request.scheme }}://{{ request.get_host }}{{ request.path }}{% endblock %}"/>
|
||||
{# Favicons #}<link rel="icon" href="{{ request.scheme }}://{{ request.get_host }}{% static 'favicon.ico' %}" type="image/x-icon" />
|
||||
{# Favicons #}<link rel="icon" type="image/png" href="{% static 'favicon-96x96.png' %}" />
|
||||
{# Favicons #}<link rel="icon" href="{% static 'favicon-light.svg' %}" type="image/svg+xml" media="(prefers-color-scheme: light)" />
|
||||
@@ -30,8 +30,8 @@
|
||||
{# Favicons #}<link rel="apple-touch-icon" sizes="180x180" href="{% static 'apple-touch-icon.png' %}" />
|
||||
{# Favicons #}<link rel="manifest" href="{% static 'site.webmanifest' %}" />
|
||||
{# Для ИИ #}<link rel="help" type="text/markdown" href="/llms.txt"/>
|
||||
{# Bootstrap 5 CSS #}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"/>
|
||||
{# Bootstrap Icons #}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"/>
|
||||
{# Bootstrap 5 CSS #}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" onerror="this.onerror=null;this.href='{% static 'vendor/bootstrap/bootstrap.min.css' %}';"/>
|
||||
{# Bootstrap Icons #}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" onerror="this.onerror=null;this.href='{% static 'vendor/bootstrap-icons/bootstrap-icons.min.css' %}';"/>
|
||||
<style>
|
||||
:root {
|
||||
--bg-image-text: url("{% static 'svg/logo-etpgrf-site-light-txt.svg' %}");
|
||||
@@ -46,8 +46,18 @@
|
||||
}
|
||||
</style>
|
||||
{# Custom CSS #}<link href="{% static 'css/etpgrf.css' %}" rel="stylesheet"/>
|
||||
{# HTMX #}<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
{# Alpine.js #}<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
{# HTMX #}<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<script>window.htmx || document.write('<script src="{% static "vendor/htmx/htmx.min.js" %}"><\/script>')</script>
|
||||
{# Alpine.js #}<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js" onerror="this.onerror=null;var s=document.createElement('script');s.defer=true;s.src='{% static 'vendor/alpinejs/cdn.min.js' %}';document.head.appendChild(s);"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.Alpine) {
|
||||
var s = document.createElement('script');
|
||||
s.src = "{% static 'vendor/alpinejs/cdn.min.js' %}";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -84,7 +94,7 @@
|
||||
<div class="container d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted small nowrap me-2">© Sergei Erjemin, 2025–{% now 'Y' %}.</span>
|
||||
<nobr class="text-muted small mx-2">
|
||||
<i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i><a href="/changelog">v0.1.6.post1 / v0.2.7</a>
|
||||
<i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i><a href="/changelog">v0.1.6.post1 / v0.2.9</a>
|
||||
</nobr>
|
||||
{# Сводная статистика (HTMX) #}<span class="text-muted small ms-2" hx-get="{% url 'stats_summary' %}" hx-trigger="load">
|
||||
...
|
||||
@@ -106,7 +116,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Bootstrap JS #}<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{# Bootstrap JS #}<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>window.bootstrap || document.write('<script src="{% static "vendor/bootstrap/bootstrap.bundle.min.js" %}"><\/script>')</script>
|
||||
{# Custom JS #}<script src="{% static 'js/base.js' %}" defer></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends 'typograph/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}ETPGRF — единая типографика для веба{% endblock %}
|
||||
{% block title %}Онлайн-типограф для текста и HTML — ETPGRF: кавычки «ёлочки», тире, неразрывные пробелы{% endblock %}
|
||||
|
||||
{% block description %}Бесплатный онлайн-типограф для подготовки текстов к публикации в вебе. Расставка неразрывных пробелов, правильных кавычек («ёлочки»), тире, спецсимволы, отбивка, компоновка, висячая пунктуация. Идеально для верстки сайтов, статей и постов.{% endblock %}
|
||||
{% block og_title %}Онлайн-типограф для текста и HTML — ETPGRF{% endblock %}
|
||||
|
||||
{% block keywords %}типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев{% endblock %}
|
||||
{% block og_description %}Бесплатный умный онлайн-типограф для подготовки текстов, статей и верстки к публикации в вебе за один клик.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
{# SEO-текст и описание #}
|
||||
<div class="mb-4">
|
||||
<h1 class="h3 mb-3">Онлайн-типограф для веба</h1>
|
||||
<h1 class="h3 mb-3">Онлайн-типограф для текста и веб-публикаций</h1>
|
||||
<p>
|
||||
Интернет-типограф <strong>etpgrf</strong> — инструмент для подготовки текста для публикации
|
||||
в вебе. Расстановка неразрывных пробелов перед союзами и предлогами, замена кавычек
|
||||
@@ -298,6 +298,192 @@
|
||||
<div id="cm-result-wrapper" class="result-box p-0"></div>
|
||||
<div id="result-area" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
{# ИНФОРМАЦИОННЫЙ И SEO-БЛОК #}
|
||||
<div class="col-md-12 mt-4 pt-3 border-top">
|
||||
<div class="text-center mb-3">
|
||||
<button class="btn btn-sm btn-outline-secondary rounded-pill px-3 py-1 shadow-sm collapse-toggle-btn collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#seoDetailsCollapse"
|
||||
aria-expanded="false"
|
||||
aria-controls="seoDetailsCollapse">
|
||||
<i class="bi bi-info-circle me-1"></i> О сервисе, возможности и правила
|
||||
<i class="bi bi-chevron-down ms-1 collapse-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse" id="seoDetailsCollapse">
|
||||
<section class="mb-5 pt-3">
|
||||
<h2 class="h4 mb-4">Возможности онлайн-типографа etpgrf</h2>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-distribute-horizontal me-1"></i> Неразрывные пробелы (&nbsp;)</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Автоматическая привязка предлогов, союзов, частиц («в», «на», «с», «и», «не», «бы»…) и артиклей („the“, „a“, „is“…). Парсер «видит» и связывает их со словами даже сквозь кавычки и скобки (например, <code>и&nbsp;«Беда» села на мель</code>).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-quote me-1"></i> Кавычки «ёлочки» и „лапки“</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Замена машинных прямых кавычек (<code>"</code>) на правильные русские «ёлочки» и французские „лапки“ для вложенных цитат. Поддержка английской, современной русской и русской дореформенной типографики («безъ», «въ», «надъ», «сiя»…).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-dash-lg me-1"></i> Тире, числа и диапазоны</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Замена дефисов на длинное тире (—) с языковой отбивкой, среднее тире (–) в числовых интервалах (1941–1945, 10–20 кг). Корректная обработка арабских и римских чисел (например, XXI век, III в. н. э.).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-person-badge me-1"></i> Инициалы и сокращения</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Связывание неразрывными пробелами инициалов (А.С. Пушкин), устойчивых сокращений (ЗАО «Рога и Копыта», др. Барменталь), сокращений («и т.д.», «и др.»), знаков (§ 5, № 12) и единиц (100 руб., 50 м²).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-layout-text-sidebar me-1"></i> Висячая пунктуация</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Оптическое выравнивание левого и правого края текстового блока за счет выноса кавычек, скобок и тире за границу полосы набора. Идеально для крупных заголовков, цитат, выносов и врезок.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-shield-check me-1"></i> Санитайзер и спецсимволы</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Встроенный HTML-санитайзер: очистка текста от мусорных тегов при копировании из Word/Google Docs, сброс разметки висячей пунктуации. Обработка псевдографики (<code>(c)</code> → ©, <code>(tm)</code> → ™, <code>+/-</code> → ±).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-fonts me-1"></i> Переносы в словах</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Автоматическая расстановка мягких переносов (<code>&shy;</code>) в длинных словах с настраиваемым порогом длины (от 12 символов). Колонки текста аккуратно перено­сятся в узких блоках и на мобильных экранах без разрыва вёрстки.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-code-slash me-1"></i> Форматы вывода и гибкие правила</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Поддержка HTML с мнемониками, смешанного режима и Unicode (UTF-8) для постов в соцсетях (VK, Telegram, Reddit). Любые группы правил можно гибко комбинировать, включать или отключать под конкретную задачу или проект.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-body-tertiary h-100 feature-card">
|
||||
<h3 class="h6 fw-bold"><i class="bi bi-infinity me-1"></i> Бесплатно и безлимитно</h3>
|
||||
<p class="small text-muted mb-0">
|
||||
Полная свобода использования без ограничений по объёму текста, без регистрации, подписок и рекламы. Доступен как онлайн-сервис, так и в виде открытой библиотеки для Python по лицензии MIT.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="mb-5">
|
||||
<h2 class="h4 mb-3">Кому подходит типограф ETPGRF</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="border-start border-3 border-secondary ps-3">
|
||||
<h3 class="h6 fw-bold">Контент-менеджерам и дизайнерам</h3>
|
||||
<p class="small text-muted">
|
||||
Профессиональный визуальный ритм и читаемость текста в веб-интерфейсах, блогах, SMM-публикациях в соцсетях и электронных рассылках.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="border-start border-3 border-secondary ps-3">
|
||||
<h3 class="h6 fw-bold">Веб-разработчикам и верстальщикам</h3>
|
||||
<p class="small text-muted">
|
||||
Получайте чистый, валидный HTML-код с неразрывными пробелами и мнемониками, готовый к вставке в шаблоны, CMS и лендинги.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="border-start border-3 border-secondary ps-3">
|
||||
<h3 class="h6 fw-bold">Редакторам и копирайтерам</h3>
|
||||
<p class="small text-muted">
|
||||
Быстрая автоматическая подготовка статей, колонок и постов для публикации без рутинной ручной расстановки спецсимволов.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="mb-5">
|
||||
<h2 class="h4 mb-3">Интеграция в проекты: Python-библиотека etpgrf</h2>
|
||||
<p class="small text-muted">
|
||||
Вы можете свободно использовать движок etpgrf-типографа офлайн в своих Python-скриптах, Django-, FastAPI- или Flask-приложениях и сайтах:
|
||||
</p>
|
||||
<div class="bg-body-tertiary p-3 rounded border font-monospace small mb-3">
|
||||
<div class="text-secondary mb-1"># Установка через pip:</div>
|
||||
<div class="text-success fw-bold mb-3">pip install etpgrf</div>
|
||||
<div class="text-secondary mb-1"># Пример использования в коде:</div>
|
||||
<div><span class="text-primary">from</span> etpgrf.main <span class="text-primary">import</span> Typographer</div>
|
||||
<div class="mt-1">tp = Typographer(lang=<span class="text-success">'ru'</span>, mode=<span class="text-success">'mixed'</span>)</div>
|
||||
<div>formatted_html = tp.process(<span class="text-success">'Правильная вёрстка "кавычек" и отбивка тире -- улучшают читаемость текста на сайте!'</span>)</div>
|
||||
</div>
|
||||
<p class="small text-muted mb-0">
|
||||
Подробная документация и исходный код библиотеки открыты на <a href="https://github.com/erjemin/etpgrf" target="_blank" rel="noopener">GitHub</a> и <a href="https://pypi.org/project/etpgrf/" target="_blank" rel="noopener">PyPI</a>.
|
||||
</p>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="mb-5">
|
||||
<h2 class="h4 mb-3">Часто задаваемые вопросы (FAQ)</h2>
|
||||
<div class="accordion" id="faqAccordion">
|
||||
<div class="accordion-item bg-body-tertiary">
|
||||
<h3 class="accordion-header" id="faqHeadingOne">
|
||||
<button class="accordion-button collapsed bg-body-tertiary" type="button" data-bs-toggle="collapse" data-bs-target="#faqOne" aria-expanded="false" aria-controls="faqOne">
|
||||
Зачем типографировать текст перед публикацией на сайте?
|
||||
</button>
|
||||
</h3>
|
||||
<div id="faqOne" class="accordion-collapse collapse" aria-labelledby="faqHeadingOne" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body small text-muted">
|
||||
В вебе строки переносятся динамически в зависимости от экрана устройства. Без неразрывных пробелов предлоги и союзы часто «висят» на концах строк, а дефисы путаются с длинным тире. Типографика делает текст легким для восприятия и профессиональным.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item bg-body-tertiary">
|
||||
<h3 class="accordion-header" id="faqHeadingTwo">
|
||||
<button class="accordion-button collapsed bg-body-tertiary" type="button" data-bs-toggle="collapse" data-bs-target="#faqTwo" aria-expanded="false" aria-controls="faqTwo">
|
||||
Чем ETPGRF отличается от Типографа Студии Лебедева или Типографа Муравьёва?
|
||||
</button>
|
||||
</h3>
|
||||
<div id="faqTwo" class="accordion-collapse collapse" aria-labelledby="faqHeadingTwo" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body small text-muted">
|
||||
ETPGRF разработан на современной открытой кодовой базе Python 3, активно поддерживается, умеет корректно обрабатывать смешанные русско-английские тексты (ru+en), имеет гибкий выбор режима кодирования (Unicode/HTML-мнемоники), настраиваемую висячую пунктуацию и может работать как локальный модуль или веб-микросервис.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item bg-body-tertiary">
|
||||
<h3 class="accordion-header" id="faqHeadingThree">
|
||||
<button class="accordion-button collapsed bg-body-tertiary" type="button" data-bs-toggle="collapse" data-bs-target="#faqThree" aria-expanded="false" aria-controls="faqThree">
|
||||
Сохраняются ли введённые тексты на сервере?
|
||||
</button>
|
||||
</h3>
|
||||
<div id="faqThree" class="accordion-collapse collapse" aria-labelledby="faqHeadingThree" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body small text-muted">
|
||||
Нет, сервис соблюдает полную конфиденциальность. Тексты обрабатываются исключительно в памяти в момент отправки запроса и не сохраняются в базу данных и логи.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
from pathlib import Path
|
||||
from django.test import TestCase, Client, override_settings
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@override_settings(SECURE_SSL_REDIRECT=False)
|
||||
class VendorAssetsAndFallbackTestCase(TestCase):
|
||||
"""
|
||||
Тестирование наличия вендорных ассетов и корректности подключения fallback в шаблоне base.html.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_vendor_static_files_exist(self):
|
||||
"""Проверяем, что все скомпилированные/скопированные локальные файлы присутствуют в статике."""
|
||||
static_dir = settings.BASE_DIR.parent / 'public' / 'static'
|
||||
|
||||
required_files = [
|
||||
'vendor/bootstrap/bootstrap.min.css',
|
||||
'vendor/bootstrap/bootstrap.bundle.min.js',
|
||||
'vendor/bootstrap-icons/bootstrap-icons.min.css',
|
||||
'vendor/bootstrap-icons/fonts/bootstrap-icons.woff2',
|
||||
'vendor/bootstrap-icons/fonts/bootstrap-icons.woff',
|
||||
'vendor/htmx/htmx.min.js',
|
||||
'vendor/alpinejs/cdn.min.js',
|
||||
'codemirror/editor.js',
|
||||
]
|
||||
|
||||
for rel_path in required_files:
|
||||
file_path = static_dir / rel_path
|
||||
self.assertTrue(
|
||||
file_path.exists(),
|
||||
f"Файл вендора не найден в статике: {rel_path} (ожидался по пути {file_path})"
|
||||
)
|
||||
self.assertGreater(
|
||||
file_path.stat().st_size,
|
||||
0,
|
||||
f"Файл вендора пуст: {rel_path}"
|
||||
)
|
||||
|
||||
def test_base_template_fallback_tags(self):
|
||||
"""Проверяем, что главная страница отдает теги с fallback-обработкой ошибок загрузки с CDN."""
|
||||
response = self.client.get(reverse('index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
content = response.content.decode('utf-8')
|
||||
|
||||
# Проверка fallback для Bootstrap CSS
|
||||
self.assertIn('bootstrap.min.css', content)
|
||||
self.assertIn("vendor/bootstrap/bootstrap.min.css", content)
|
||||
|
||||
# Проверка fallback для Bootstrap Icons
|
||||
self.assertIn('bootstrap-icons.min.css', content)
|
||||
self.assertIn("vendor/bootstrap-icons/bootstrap-icons.min.css", content)
|
||||
|
||||
# Проверка fallback для HTMX
|
||||
self.assertIn('htmx.org', content)
|
||||
self.assertIn('window.htmx || document.write', content)
|
||||
self.assertIn('vendor/htmx/htmx.min.js', content)
|
||||
|
||||
# Проверка fallback для Alpine.js
|
||||
self.assertIn('alpinejs', content)
|
||||
self.assertIn('vendor/alpinejs/cdn.min.js', content)
|
||||
|
||||
# Проверка fallback для Bootstrap JS
|
||||
self.assertIn('bootstrap.bundle.min.js', content)
|
||||
self.assertIn('window.bootstrap || document.write', content)
|
||||
self.assertIn('vendor/bootstrap/bootstrap.bundle.min.js', content)
|
||||
|
||||
|
||||
@override_settings(SECURE_SSL_REDIRECT=False)
|
||||
class TypographViewsTestCase(TestCase):
|
||||
"""
|
||||
Базовые тесты функциональности представлений типографа.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_process_text_endpoint(self):
|
||||
"""Проверка эндпоинта типографирования текста."""
|
||||
response = self.client.post(
|
||||
reverse('process_text'),
|
||||
data={
|
||||
'text': 'Тест "кавычек" и предлога в тексте.',
|
||||
'langs': 'ru',
|
||||
'quotes': 'on',
|
||||
'unbreakables': 'on',
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('X-Processing-Time', response.headers)
|
||||
content = response.content.decode('utf-8')
|
||||
# Проверяем типографирование кавычек «ёлочки»
|
||||
self.assertIn('«кавычек»', content)
|
||||
|
||||
def test_stats_summary_endpoint(self):
|
||||
"""Проверка эндпоинта сводной статистики."""
|
||||
response = self.client.get(reverse('stats_summary'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_seo_metadata_and_sitemap(self):
|
||||
"""Проверка мета-тегов на главной странице и генерации sitemap.xml."""
|
||||
response = self.client.get(reverse('index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
content = response.content.decode('utf-8')
|
||||
self.assertIn('<title>Онлайн-типограф', content)
|
||||
self.assertIn('name="description"', content)
|
||||
self.assertIn('name="keywords"', content)
|
||||
self.assertIn('rel="canonical"', content)
|
||||
|
||||
# Проверка sitemap.xml
|
||||
sitemap_res = self.client.get('/sitemap.xml')
|
||||
self.assertEqual(sitemap_res.status_code, 200)
|
||||
sitemap_content = sitemap_res.content.decode('utf-8')
|
||||
self.assertIn('<loc>', sitemap_content)
|
||||
self.assertIn('http://testserver/', sitemap_content)
|
||||
Regular → Executable
+16
-16
@@ -1,24 +1,24 @@
|
||||
#!/usr/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Необходимо предварительно установить NPM и esbuild:
|
||||
# Переходим в директорию скрипта
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Необходимо предварительно установить Node.js и npm:
|
||||
# Для Ubuntu/Debian:
|
||||
# sudo apt install npm
|
||||
# npm install -g esbuild
|
||||
# sudo apt install nodejs npm
|
||||
# Для MacOS (через Homebrew):
|
||||
# brew install npm
|
||||
# npm install -g esbuild
|
||||
# brew install node
|
||||
# Для Windows:
|
||||
# Скачайте и установите Node.js с официального сайта: https://nodejs.org/
|
||||
# Затем установите esbuild глобально:
|
||||
# npm install -g esbuild
|
||||
|
||||
|
||||
# Устанавливаем CodeMirror и необходимые пакеты
|
||||
npm install \ [±main ●●]
|
||||
@codemirror/lang-html \
|
||||
@codemirror/theme-one-dark \
|
||||
@codemirror/commands \
|
||||
@codemirror/language
|
||||
# Устанавливаем зависимости
|
||||
npm install
|
||||
|
||||
# Собираем js в каталог ./public/static/codemirror/editor.js с помощью esbuild
|
||||
npx esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js
|
||||
# Собираем всё (CodeMirror и копирование вендорных библиотек в public/static/vendor/)
|
||||
npm run build
|
||||
|
||||
# Подчищаем за собой node_modules
|
||||
echo "Очистка node_modules..."
|
||||
rm -rf node_modules
|
||||
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Целевая папка для статики вендоров
|
||||
const targetDir = path.resolve(__dirname, '../public/static/vendor');
|
||||
|
||||
// Список файлов и директорий для копирования
|
||||
const itemsToCopy = [
|
||||
// Bootstrap 5 (CSS & JS bundle)
|
||||
{
|
||||
src: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
|
||||
dest: 'bootstrap/bootstrap.min.css'
|
||||
},
|
||||
{
|
||||
src: 'node_modules/bootstrap/dist/css/bootstrap.min.css.map',
|
||||
dest: 'bootstrap/bootstrap.min.css.map'
|
||||
},
|
||||
{
|
||||
src: 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js',
|
||||
dest: 'bootstrap/bootstrap.bundle.min.js'
|
||||
},
|
||||
{
|
||||
src: 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map',
|
||||
dest: 'bootstrap/bootstrap.bundle.min.js.map'
|
||||
},
|
||||
|
||||
// Bootstrap Icons (CSS & WOFF/WOFF2 шрифты)
|
||||
{
|
||||
src: 'node_modules/bootstrap-icons/font/bootstrap-icons.min.css',
|
||||
dest: 'bootstrap-icons/bootstrap-icons.min.css'
|
||||
},
|
||||
{
|
||||
src: 'node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2',
|
||||
dest: 'bootstrap-icons/fonts/bootstrap-icons.woff2'
|
||||
},
|
||||
{
|
||||
src: 'node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff',
|
||||
dest: 'bootstrap-icons/fonts/bootstrap-icons.woff'
|
||||
},
|
||||
|
||||
// HTMX
|
||||
{
|
||||
src: 'node_modules/htmx.org/dist/htmx.min.js',
|
||||
dest: 'htmx/htmx.min.js'
|
||||
},
|
||||
|
||||
// Alpine.js
|
||||
{
|
||||
src: 'node_modules/alpinejs/dist/cdn.min.js',
|
||||
dest: 'alpinejs/cdn.min.js'
|
||||
}
|
||||
];
|
||||
|
||||
console.log('Сборка и копирование вендорных библиотек в static/vendor/...');
|
||||
|
||||
let copiedCount = 0;
|
||||
|
||||
for (const item of itemsToCopy) {
|
||||
const srcPath = path.resolve(__dirname, item.src);
|
||||
const destPath = path.join(targetDir, item.dest);
|
||||
|
||||
if (!fs.existsSync(srcPath)) {
|
||||
console.warn(`Предупреждение: исходный файл не найден: ${item.src}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Создаем папку назначения, если не существует
|
||||
const destDirPath = path.dirname(destPath);
|
||||
if (!fs.existsSync(destDirPath)) {
|
||||
fs.mkdirSync(destDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
console.log(` Скопировано: ${item.src} -> ${path.relative(path.resolve(__dirname, '..'), destPath)}`);
|
||||
copiedCount++;
|
||||
}
|
||||
|
||||
console.log(`Успешно скопировано файлов: ${copiedCount}`);
|
||||
Generated
+83
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "tmp-frontend",
|
||||
"name": "frontend-assembly",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tmp-frontend",
|
||||
"name": "frontend-assembly",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -13,7 +13,11 @@
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"codemirror": "^6.0.2"
|
||||
"alpinejs": "^3.14.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"codemirror": "^6.0.2",
|
||||
"htmx.org": "^1.9.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.2"
|
||||
@@ -662,6 +666,76 @@
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/alpinejs": {
|
||||
"version": "3.14.8",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.8.tgz",
|
||||
"integrity": "sha512-wT2fuP2DXpGk/jKaglwy7S/IJpm1FD+b7U6zUrhwErjoq5h27S4dxkJEXVvhbdwyPv9U+3OkUuNLkZT4h2Kfrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "~3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap-icons": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
|
||||
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
@@ -725,6 +799,12 @@
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/htmx.org": {
|
||||
"version": "1.9.12",
|
||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz",
|
||||
"integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"description": "CodeMirror build for ETPGRF",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "esbuild src/editor.js --bundle --format=esm --outfile=dist/editor.js --minify",
|
||||
"dev": "esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js --minify",
|
||||
"build:codemirror": "esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js --minify",
|
||||
"build:vendor": "node copy-vendor.js",
|
||||
"build": "npm run build:codemirror && npm run build:vendor",
|
||||
"dev": "npm run build",
|
||||
"watch": "esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -17,7 +19,11 @@
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"codemirror": "^6.0.2"
|
||||
"alpinejs": "^3.14.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"codemirror": "^6.0.2",
|
||||
"htmx.org": "^1.9.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.2"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -410,3 +410,44 @@ footer.footer a:hover {
|
||||
border-right: 1px solid var(--bs-border-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Карточки возможностей (hover-эффект без потери читаемости) --- */
|
||||
.feature-card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(var(--bs-primary-rgb), 0.5) !important;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.09), 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .feature-card:hover {
|
||||
border-color: rgba(176, 190, 197, 0.5) !important;
|
||||
background-color: rgba(255, 255, 255, 0.03) !important;
|
||||
box-shadow: 0 6px 22px rgba(255, 255, 255, 0.15), 0 2px 8px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-bs-theme="light"]) .feature-card:hover {
|
||||
border-color: rgba(176, 190, 197, 0.5) !important;
|
||||
background-color: rgba(255, 255, 255, 0.03) !important;
|
||||
box-shadow: 0 6px 22px rgba(255, 255, 255, 0.15), 0 2px 8px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Кнопка раскрытия информационного блока --- */
|
||||
.collapse-toggle-btn {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.collapse-toggle-btn .collapse-icon {
|
||||
display: inline-block;
|
||||
transition: transform 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.collapse-toggle-btn:not(.collapsed) .collapse-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
+5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "2026-etpgrf-site"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
description = ""
|
||||
authors = ["erjemin <erjemin@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
Reference in New Issue
Block a user