diff --git a/etpgrf_site/typograph/templates/typograph/base.html b/etpgrf_site/typograph/templates/typograph/base.html index a3e0026..7e61dea 100644 --- a/etpgrf_site/typograph/templates/typograph/base.html +++ b/etpgrf_site/typograph/templates/typograph/base.html @@ -30,8 +30,8 @@ {# Favicons #} {# Favicons #} {# Для ИИ #} - {# Bootstrap 5 CSS #} - {# Bootstrap Icons #} + {# Bootstrap 5 CSS #} + {# Bootstrap Icons #} {# Custom CSS #} - {# HTMX #} - {# Alpine.js #} + {# HTMX #} + + {# Alpine.js #} +
@@ -106,7 +116,8 @@ -{# Bootstrap JS #} +{# Bootstrap JS #} + {# Custom JS #} {% block scripts %}{% endblock %} diff --git a/etpgrf_site/typograph/templates/typograph/index.html b/etpgrf_site/typograph/templates/typograph/index.html index 6efdefd..0a97211 100644 --- a/etpgrf_site/typograph/templates/typograph/index.html +++ b/etpgrf_site/typograph/templates/typograph/index.html @@ -3,7 +3,7 @@ {% block title %}ETPGRF — единая типографика для веба{% endblock %} -{% block description %}Бесплатный онлайн-типограф для подготовки текстов к публикации в вебе. Расставка неразрывных пробелов, правильных кавычек («ёлочки»), тире, спецсимволы, отбивка, компоновка, висячая пунктуация. Идеально для верстки сайтов, статей и постов.{% endblock %} +{% block description %}Бесплатный онлайн-типограф для подготовки публикаций в вебе: кавычки «ёлочки», тире, неразрывные пробелы, спецсимволы, отбивка/компоновка, висячая пунктуация… Идеально для верстки сайтов, статей и постов.{% endblock %} {% block keywords %}типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев{% endblock %} diff --git a/etpgrf_site/typograph/tests.py b/etpgrf_site/typograph/tests.py new file mode 100644 index 0000000..9f4eac9 --- /dev/null +++ b/etpgrf_site/typograph/tests.py @@ -0,0 +1,102 @@ +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) diff --git a/frontend-assembly/assembly_via_npm.sh b/frontend-assembly/assembly_via_npm.sh old mode 100644 new mode 100755 index 06cac7b..f346ed4 --- a/frontend-assembly/assembly_via_npm.sh +++ b/frontend-assembly/assembly_via_npm.sh @@ -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 \ No newline at end of file +# Собираем всё (CodeMirror и копирование вендорных библиотек в public/static/vendor/) +npm run build + +# Подчищаем за собой node_modules +echo "Очистка node_modules..." +rm -rf node_modules \ No newline at end of file diff --git a/frontend-assembly/copy-vendor.js b/frontend-assembly/copy-vendor.js new file mode 100644 index 0000000..9813044 --- /dev/null +++ b/frontend-assembly/copy-vendor.js @@ -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}`); diff --git a/frontend-assembly/package-lock.json b/frontend-assembly/package-lock.json index ad25d1f..a16482a 100644 --- a/frontend-assembly/package-lock.json +++ b/frontend-assembly/package-lock.json @@ -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", diff --git a/frontend-assembly/package.json b/frontend-assembly/package.json index 2a52a8b..9d730de 100644 --- a/frontend-assembly/package.json +++ b/frontend-assembly/package.json @@ -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"