diff --git a/dicquo/web/admin.py b/dicquo/web/admin.py
index 01e23f6..28d3ac6 100644
--- a/dicquo/web/admin.py
+++ b/dicquo/web/admin.py
@@ -1,10 +1,72 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
+from django import forms
from web.models import TbDictumAndQuotes, TbAuthor, TbImages, TbOrigin
+try:
+ from etpgrf.typograph import Typographer
+ from etpgrf.layout import LayoutProcessor
+ from etpgrf.hyphenation import Hyphenator
+except ImportError:
+ # Заглушка, если библиотека не установлена
+ class Typographer:
+ def __init__(self, **kwargs): pass
+ def process(self, text): return text
+ class LayoutProcessor:
+ def __init__(self, **kwargs): pass
+ class Hyphenator:
+ def __init__(self, **kwargs): pass
+
+
+class DictumAdminForm(forms.ModelForm):
+ # Виртуальные поля для настройки типографа
+ etp_language = forms.ChoiceField(
+ label="Язык типографики",
+ choices=[('ru', 'Русский'), ('en', 'English'), ('ru,en', 'Ru + En')],
+ initial='ru',
+ required=False
+ )
+ etp_quotes = forms.BooleanField(
+ label="Обработка кавычек",
+ initial=True,
+ required=False,
+ help_text="Заменять прямые кавычки на «ёлочки» или “лапки”"
+ )
+ etp_hanging_punctuation = forms.ChoiceField(
+ label="Висячая пунктуация",
+ choices=[('no', 'Нет'), ('left', 'Слева'), ('right', 'Справа'), ('both', 'Обе стороны')],
+ initial='left',
+ required=False,
+ help_text="Выносить кавычки за границу текстового блока"
+ )
+ etp_hyphenation = forms.BooleanField(
+ label="Переносы",
+ initial=True,
+ required=False,
+ help_text="Расставлять мягкие переносы (­)"
+ )
+ etp_sanitize = forms.BooleanField(
+ label="Санитайзер (HTML)",
+ initial=False,
+ required=False,
+ help_text="Очищать HTML теги перед обработкой"
+ )
+ etp_mode = forms.ChoiceField(
+ label="Режим вывода",
+ choices=[('mixed', 'Смешанный (Mixed)'), ('unicode', 'Юникод (Unicode)'), ('mnemonic', 'Мнемоники')],
+ initial='mixed',
+ required=False,
+ help_text="Формат спецсимволов"
+ )
+
+ class Meta:
+ model = TbDictumAndQuotes
+ fields = '__all__'
+
# Register your models here.
class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
+ form = DictumAdminForm
search_fields = ['id', 'szIntro', 'szContent', ]
list_display = ('id', 'szIntro', 'szContent', 'tag_list', 'iViewCounter', 'dtEdited', )
list_display_links = ('id', 'szIntro', 'szContent', )
@@ -13,12 +75,109 @@ class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
actions_on_top = False
actions_on_bottom = True
actions_selection_counter = True
- # погасить кнопку "Добавить" в интерфейсе админки
- # def has_add_permission(self, request):
- # return False
- # fieldsets = (
- # (None, {'fields': ('szIntro', 'iViewCounter', 'tags',)}),
- # )
+
+ fieldsets = (
+ (None, {
+ 'fields': ('szIntro', 'szContent', 'kAuthor', 'kOrigin', 'kImages', 'tags', 'bIsChecked')
+ }),
+ ('Настройки типографа (Etpgrf)', {
+ 'classes': ('collapse',),
+ 'fields': (
+ ('etp_language', 'etp_mode'),
+ ('etp_quotes', 'etp_sanitize'),
+ ('etp_hyphenation', 'etp_hanging_punctuation'),
+ ),
+ 'description': 'Настройки применяются при сохранении. Результат записывается в скрытые HTML-поля.'
+ }),
+ ('HTML Результат (ReadOnly)', {
+ 'classes': ('collapse',),
+ 'fields': ('szIntroHTML', 'szContentHTML'),
+ }),
+ ('Служебное', {
+ 'classes': ('collapse',),
+ 'fields': ('iViewCounter', 'imFileOG', 'bTypograph') # bTypograph kept for compatibility
+ })
+ )
+ readonly_fields = ('szIntroHTML', 'szContentHTML', 'iViewCounter')
+
+ def save_model(self, request, obj, form, change):
+ # 1. Читаем базовые настройки
+ langs = form.cleaned_data.get('etp_language', 'ru').split(',')
+
+ # 2. Собираем LayoutProcessor
+ layout_option = False
+ # Включаем layout по умолчанию с базовыми настройками (инициалы, юниты)
+ layout_option = LayoutProcessor(
+ langs=langs,
+ process_initials_and_acronyms=True,
+ process_units=True
+ )
+
+ # 3. Собираем Hyphenator
+ hyphenation_enabled = form.cleaned_data.get('etp_hyphenation', True)
+ hyphenation_option = False
+ if hyphenation_enabled:
+ hyphenation_option = Hyphenator(
+ langs=langs,
+ max_unhyphenated_len=12
+ )
+
+ # 4. Читаем Sanitizer
+ sanitizer_enabled = form.cleaned_data.get('etp_sanitize', False)
+ sanitizer_option = None
+ if sanitizer_enabled:
+ sanitizer_option = 'etp'
+
+ # 5. Читаем Hanging Punctuation
+ hanging_val = form.cleaned_data.get('etp_hanging_punctuation', 'no')
+ hanging_option = None
+ if hanging_val != 'no':
+ hanging_option = hanging_val
+
+ # 6. Собираем общие опции
+ options = {
+ 'langs': langs,
+ 'process_html': True,
+ 'quotes': form.cleaned_data.get('etp_quotes', True),
+ 'layout': layout_option,
+ 'unbreakables': True,
+ 'hyphenation': hyphenation_option,
+ 'symbols': True,
+ 'hanging_punctuation': hanging_option,
+ 'mode': form.cleaned_data.get('etp_mode', 'mixed'),
+ 'sanitizer': sanitizer_option,
+ }
+
+ # Инициализируем типограф с настройками из формы
+ try:
+ # DEBUG: Проверка, какой класс используется
+ if Typographer.__module__ == __name__: # Если класс определен в этом же файле (заглушка)
+ self.message_user(request, "ВНИМАНИЕ: Используется заглушка Typographer! Библиотека etpgrf не найдена.", level='WARNING')
+
+ t = Typographer(**options)
+
+ # Обрабатываем контент
+ if obj.szContent:
+ # В онлайн-типографе используется .process(text)
+ old_html = obj.szContentHTML or ""
+ processed = t.process(obj.szContent)
+ obj.szContentHTML = processed
+
+ # DEBUG: Проверка изменений
+ if processed != old_html and processed != obj.szContent:
+ self.message_user(request, f"Типограф: szContentHTML обновлен (len changed: {len(old_html)} -> {len(processed)})", level='INFO')
+
+ # Обрабатываем интро
+ if obj.szIntro:
+ obj.szIntroHTML = t.process(obj.szIntro)
+
+ except Exception as e:
+ # Fallback if processing fails
+ self.message_user(request, f"Ошибка типографа: {e}", level='ERROR')
+ if not obj.szContentHTML: obj.szContentHTML = obj.szContent
+ if not obj.szIntroHTML: obj.szIntroHTML = obj.szIntro
+
+ super().save_model(request, obj, form, change)
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('tags')
@@ -64,3 +223,4 @@ admin.site.register(TbDictumAndQuotes, AdmDictumAndQuotesAdmin)
admin.site.register(TbOrigin, AdmOrigin)
admin.site.register(TbImages, AdmImages)
admin.site.register(TbAuthor, AdmAuthor)
+
diff --git a/dicquo/web/migrations/0003_alter_tbdictumandquotes_btypograph_and_more.py b/dicquo/web/migrations/0003_alter_tbdictumandquotes_btypograph_and_more.py
new file mode 100644
index 0000000..783a9ce
--- /dev/null
+++ b/dicquo/web/migrations/0003_alter_tbdictumandquotes_btypograph_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 6.0.2 on 2026-02-18 19:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web', '0002_tbdictumandquotes_bischecked'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='tbdictumandquotes',
+ name='bTypograph',
+ field=models.BooleanField(db_index=True, default=True, help_text='Применять типографику?', verbose_name='Типографировать'),
+ ),
+ migrations.AlterField(
+ model_name='tbdictumandquotes',
+ name='szContent',
+ field=models.TextField(default='', help_text='Не обязательно.', max_length=640, verbose_name='Изречение'),
+ ),
+ migrations.AlterField(
+ model_name='tbdictumandquotes',
+ name='szContentHTML',
+ field=models.TextField(blank=True, default='', help_text='Содержание цитаты, афоризма, высказывания…
Свёрстано в HTML по правилам типографики', verbose_name='Изречение HTML'),
+ ),
+ migrations.AlterField(
+ model_name='tbdictumandquotes',
+ name='szIntroHTML',
+ field=models.TextField(blank=True, default='', help_text='Автор и, если необходимо, краткая справка
Вступление перед цитатой, в HTML по правилам типографики', verbose_name='Вступление HTML'),
+ ),
+ ]
diff --git a/dicquo/web/models.py b/dicquo/web/models.py
index 8c3bf4b..79a434a 100644
--- a/dicquo/web/models.py
+++ b/dicquo/web/models.py
@@ -8,8 +8,6 @@ except ImportError:
def en_typus(text): return text
def ru_typus(text): return text
from pathlib import Path
-# import urllib3
-import json
import pytils
@@ -243,22 +241,11 @@ class TbAuthor(models.Model):
return self.__str__()
def save(self, *args, **kwargs):
- # http = urllib3.PoolManager()
- # последовательно
- # Используем типограф typus (https://github.com/byashimov/typus)
- # Используем типограф Eugene Spearance (http://www.typograf.ru/)
- # Используем типограф Муравьева (http://mdash.ru/api.v1.php)
- self.szAuthor = ru_typus(self.szAuthor)
- # resp = http.request("POST",
- # "http://www.typograf.ru/webservice/",
- # fields={"text": self.szAuthor.encode('cp1251')})
- # self.szAuthorHTML = resp.data.decode('cp1251')
- # # print(self.szContentHTML)
- # resp = http.request("POST",
- # "http://mdash.ru/api.v1.php",
- # fields={"text": self.szAuthorHTML.encode('utf-8')})
- # self.szAuthorHTML = json.loads(resp.data)["result"]
- # # print(self.szContentHTML)
+ # Типографирование перенесено в админку (через библиотеку etpgrf)
+ # Здесь оставляем только базовое сохранение
+ if not self.szAuthorHTML and self.szAuthor:
+ # Если HTML пуст, временно заполняем его оригиналом (или можно вызвать etpgrf с дефолтами)
+ self.szAuthorHTML = self.szAuthor
super(TbAuthor, self).save(*args, **kwargs)
class Meta:
@@ -294,25 +281,25 @@ class TbDictumAndQuotes(models.Model):
blank=True,
verbose_name=u"Вступление HTML",
help_text=u"Автор и, если необходимо, краткая справка
"
- u"Вступление перед цитатой, в HTML по правилам типографики"
+ u" Вступление перед цитатой, в HTML по правилам типографики"
)
szContent = models.TextField(
- max_length=256,
+ max_length=640,
default="",
- verbose_name=u"Высказывание",
- help_text=u"Не обязательно. Вступление перед цитатой."
+ verbose_name=u"Изречение",
+ help_text=u"Не обязательно."
)
szContentHTML = models.TextField(
default="",
blank=True,
verbose_name=u"Изречение HTML",
help_text=u"Содержание цитаты, афоризма, высказывания…
"
- u"Свертано в HTML по правилам типографики"
+ u" Свёрстано в HTML по правилам типографики"
)
bTypograph = models.BooleanField(
default=True,
db_index=True,
- verbose_name=u"Типографить",
+ verbose_name=u"Типографировать",
help_text=u"Применять типографику?"
)
bIsChecked = models.BooleanField(
@@ -399,41 +386,12 @@ class TbDictumAndQuotes(models.Model):
return self.__str__()
def save(self, *args, **kwargs):
- # http = urllib3.PoolManager()
- # последовательно
- # Используем типограф typus (https://github.com/byashimov/typus)
- # Используем типограф Eugene Spearance (http://www.typograf.ru/)
- # Используем типограф Муравьева (http://mdash.ru/api.v1.php)
- # if self.szIntro != "" and self.szIntro != ru_typus(self.szIntro):
- # # сравнение self.szIntro != ru_typus(self.szIntro) нужно для избежания повторных обращений
- # # к типографам при обновлении щетчиков просмотра
- # self.szIntro = ru_typus(self.szIntro)
- # resp = http.request("POST",
- # "http://www.typograf.ru/webservice/",
- # fields={"text": self.szIntro.replace("\u202f", " ").replace("\u2009", " ").encode('cp1251')})
- # self.szIntroHTML = resp.data.decode('cp1251')
- # # print(self.szIntroHTML)
- # resp = http.request("POST",
- # "http://mdash.ru/api.v1.php",
- # fields={"text": self.szIntroHTML.encode('utf-8')})
- # self.szIntroHTML = json.loads(resp.data)["result"]
- # # print(self.szIntroHTML)
- # else:
- # self.szIntroHTML = ""
- # if self.szContent != ru_typus(self.szContent):
- # # self.szContent != ru_typus(self.szContent) нужно для избежания повторных обращений
- # # к типографам при обновлении щетчиков просмотра
- # self.szContent = ru_typus(self.szContent)
- # resp = http.request("POST",
- # "http://www.typograf.ru/webservice/",
- # fields={"text": self.szContent.replace("\u202f", " ").replace("\u2009", " ").encode('cp1251')})
- # self.szContentHTML = resp.data.decode('cp1251')
- # print(self.szContentHTML)
- # resp = http.request("POST",
- # "http://mdash.ru/api.v1.php",
- # fields={"text": self.szContentHTML.encode('utf-8')})
- # self.szContentHTML = json.loads(resp.data)["result"]
- # # print(self.szContentHTML)
+ # Типографирование (szContent -> szContentHTML, szIntro -> szIntroHTML)
+ # перенесено в админку для управления параметрами (язык, переносы и т.д.)
+ if not self.szContentHTML and self.szContent:
+ self.szContentHTML = self.szContent
+ if not self.szIntroHTML and self.szIntro:
+ self.szIntroHTML = self.szIntro
super(TbDictumAndQuotes, self).save(*args, **kwargs)
class Meta:
diff --git a/public/static/css/dicquo.css b/public/static/css/dicquo.css
index b093a66..ccd2bde 100644
--- a/public/static/css/dicquo.css
+++ b/public/static/css/dicquo.css
@@ -205,3 +205,20 @@ header {
margin-bottom: 2vh;
}
}
+
+/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
+/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
+.etp-laquo { margin-left: -0.44em; } /* « */
+.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ „ */
+.etp-lsquo { margin-left: -0.22em; } /* ‘ */
+.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.25em; } /* ( [ { */
+
+/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по правому краю) --- */
+/* Общая механика: "вырываем" символ из потока для идеального выравнивания текста */
+[class^="etp-r"], [class*=" etp-r"] { position: absolute; }
+/* Точечная настройка смещения для каждого символа */
+.etp-raquo { right: -0.44em; } /* » */
+.etp-rdquo { right: -0.4em; } /* ” */
+.etp-rsquo { right: -0.22em; } /* ’ */
+.etp-rpar, .etp-rsqb, .etp-rcub { right: -0.25em; } /* ) ] } */
+.etp-r-dot, .etp-r-comma, .etp-r-colon { right: -0.15em; } /* . , : */