Files
progcode/web/scripts/buildDatabase.mjs
T

584 lines
69 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const webRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
const sourceRoot = join(webRoot, '..', '.webarchive');
const illustrationsDir = join(webRoot, 'public', 'assets', 'illustrations');
const mediaDir = join(webRoot, 'public', 'assets', 'media');
const dataDir = join(webRoot, 'data');
const rawArticles = JSON.parse(await readFile(join(sourceRoot, 'extracted-articles.json'), 'utf8'));
const categories = {
'вирус-самопроизвольный-запуск-брауз': ['Windows', 'Вирусы'],
'использование-jquery-в-webpack': ['JavaScript'],
'компиляция-64-x-разрядных-программ-на-dmd-по': ['DLang', 'DMD', 'Windows'],
'новый-движок-ctfe': ['DLang', 'Compile-time'],
'особенности-vibe-d': ['DLang', 'Vibe.d'],
'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': ['PHP', 'SSL'],
'о-tilix-и-d-интервью-с-геральдом-нанном': ['DLang', 'Интервью'],
'пишем-аналог-функции-php-preg_match_all-на-языке-прог': ['DLang', 'Регулярные выражения'],
'bitrix-api-создание-добавление-торгового-пре': ['Bitrix', 'PHP'],
'bitrix-api-функция-для-генерации-кода-элемент': ['Bitrix', 'PHP'],
'bitrix-api-add-foto-editor': ['Bitrix', 'PHP'],
'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': ['DLang', 'GC'],
'firefox-увеличение-ожидания-загрузки-timeout-стр': ['Firefox', 'Настройки'],
};
const covers = {
'вирус-самопроизвольный-запуск-брауз': '/assets/illustrations/cover-virus.svg',
'использование-jquery-в-webpack': '/assets/illustrations/jquery-webpack.svg',
'компиляция-64-x-разрядных-программ-на-dmd-по': '/assets/illustrations/dmd-win64.svg',
'новый-движок-ctfe': '/assets/illustrations/ctfe-engine.svg',
'особенности-vibe-d': '/assets/illustrations/vibe-cover.svg',
'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': '/assets/illustrations/php-ssl-cacert.svg',
'о-tilix-и-d-интервью-с-геральдом-нанном': '/assets/illustrations/tilix-cover.svg',
'пишем-аналог-функции-php-preg_match_all-на-языке-прог': '/assets/illustrations/preg-match-all-d.svg',
'bitrix-api-создание-добавление-торгового-пре': '/assets/illustrations/bitrix-offer-api.svg',
'bitrix-api-функция-для-генерации-кода-элемент': '/assets/illustrations/bitrix-translit-api.svg',
'bitrix-api-add-foto-editor': '/assets/illustrations/bitrix-photo-editor.svg',
'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': '/assets/illustrations/dconf-gc-cover.svg',
'firefox-увеличение-ожидания-загрузки-timeout-стр': '/assets/illustrations/firefox-timeout.svg',
};
const imageMap = [
['2017-05-13_13-39-44', '/assets/illustrations/taskschd-run.svg', 'Окно запуска taskschd.msc'],
['2017-05-13_13-41-17', '/assets/illustrations/taskschd-action.svg', 'Подозрительное задание в планировщике Windows'],
['/2017/05/1.png', '/assets/illustrations/vibe-async.svg', 'Асинхронная модель vibe.d'],
['/2017/05/2.png', '/assets/illustrations/vibe-api.svg', 'Короткий API vibe.d'],
['/2017/05/3.png', '/assets/illustrations/vibe-web.svg', 'Веб-стек vibe.d'],
['/2017/05/4.png', '/assets/illustrations/vibe-db.svg', 'Драйверы баз данных vibe.d'],
['/2017/05/5.png', '/assets/illustrations/vibe-io.svg', 'Неблокирующий ввод-вывод'],
['/2017/05/6.png', '/assets/illustrations/vibe-dlang.svg', 'Язык D как основа vibe.d'],
['/2017/05/7.png', '/assets/illustrations/vibe-diet.svg', 'Шаблоны Diet'],
['tilix-screenshot-2', '/assets/illustrations/tilix-screenshot.svg', 'Боковая панель Tilix в действии'],
['/2017/06/1-300x225', '/assets/illustrations/gc-small-pools.svg', 'Пулы малых объектов в GC D'],
['/2017/06/1.jpg', '/assets/illustrations/gc-small-pools.svg', 'Пулы малых объектов в GC D'],
['/2017/06/2-200x300', '/assets/illustrations/gc-large-pool.svg', 'Пул больших объектов в GC D'],
['/2017/06/2.jpg', '/assets/illustrations/gc-large-pool.svg', 'Пул больших объектов в GC D'],
['bitrix-foto-editor-2', '/assets/illustrations/bitrix-photo-editor-ui.svg', 'Редактор картинок Bitrix'],
];
const additions = {
'вирус-самопроизвольный-запуск-брауз': `
<h2>Проверяем, что проблема решена</h2>
<p>После удаления задания перезагрузите компьютер и оставьте систему включенной на тот промежуток времени, через который раньше открывался браузер. Если окно больше не появляется, значит источник найден правильно. Если запуск повторяется, проверьте не только библиотеку планировщика, но и вкладку автозагрузки, ключи Run в реестре и расширения браузера.</p>
<ul>
<li>В планировщике смотрите не только имя задания, но и действие: путь к браузеру и URL в аргументах.</li>
<li>Удаляйте только понятные вредоносные задания. Системные задачи Microsoft лучше сначала отключить, а не стирать.</li>
<li>После очистки обновите антивирус и выполните полную проверку, потому что задание могло быть лишь следом установщика.</li>
</ul>
<p>В итоге рецепт простой: найти периодический запуск, проверить действие задания, удалить его и убедиться, что источник не восстанавливается после перезагрузки.</p>`,
'использование-jquery-в-webpack': `
<h2>Итоговая схема подключения</h2>
<p>Для старого проекта jQuery в Webpack лучше подключать явно: установить пакет, импортировать его в точке входа и, если плагинам нужна глобальная переменная, добавить ProvidePlugin. Тогда один и тот же экземпляр библиотеки будет использоваться и вашим кодом, и плагинами.</p>
<pre><code>const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
}),
],
};</code></pre>
<p>Если плагин всё равно не видит jQuery, проверьте порядок импортов и отсутствие второй копии библиотеки в bundle. На этом обычно все проблемы заканчиваются.</p>`,
'компиляция-64-x-разрядных-программ-на-dmd-по': `
<h2>Короткая памятка</h2>
<p>Чтобы не возвращаться к этой настройке каждый раз, держите отдельный build-скрипт для 64-битной сборки и явно указывайте архитектуру, компилятор и путь к библиотекам. Главная мысль статьи: не смешивать 32-битные OMF-библиотеки и 64-битную сборку.</p>
<pre><code>dmd -m64 main.d -ofapp.exe
dub build --arch=x86_64</code></pre>
<p>Если линковщик ругается на формат библиотек, сначала проверьте разрядность каждой внешней зависимости. В большинстве случаев ошибка не в D-коде, а в том, что рядом оказалась библиотека другого формата.</p>`,
'новый-движок-ctfe': `
<h2>Как применять выводы на практике</h2>
<p>CTFE полезен там, где результат можно посчитать один раз во время компиляции: таблицы констант, парсинг небольших DSL, генерация однотипного кода. Но не стоит превращать компиляцию в полноценный рантайм. Чем проще входные данные и чем меньше побочных эффектов, тем легче будет сопровождать проект.</p>
<p>Практическое правило такое: если вычисление зависит только от литералов и конфигурации сборки, переносите его в CTFE. Если нужен ввод-вывод, сеть или состояние окружения, оставьте это обычному коду.</p>`,
'особенности-vibe-d': `
<h2>Когда выбирать vibe.d</h2>
<p>vibe.d хорошо подходит для небольших и средних веб-сервисов на D, где важны неблокирующий ввод-вывод, простая маршрутизация и единая экосистема DUB. Начать можно с минимального HTTP-сервера, затем добавить Diet-шаблоны, MongoDB или Redis и вынести повторяющиеся части в отдельные модули.</p>
<pre><code>shared static this()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
listenHTTP(settings, &handleRequest);
}</code></pre>
<p>Если проект требует большого количества готовых интеграций, заранее проверьте наличие пакетов в DUB. Если же нужна компактная серверная часть на D, vibe.d остаётся прямым и понятным выбором.</p>`,
'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': `
<h2>Финальная проверка настройки</h2>
<p>После подключения файла сертификатов перезапустите веб-сервер или PHP-FPM и выполните тестовый HTTPS-запрос из того же окружения, где возникала ошибка. Важно проверять не браузер, а именно PHP: CLI и веб-сервер могут использовать разные php.ini.</p>
<pre><code>php -i | grep cafile
php -r "var_dump(file_get_contents('https://example.com') !== false);"</code></pre>
<p>Не отключайте проверку SSL через CURLOPT_SSL_VERIFYPEER = false как постоянное решение. Это скрывает проблему и делает соединение уязвимым. Правильный путь — актуальный CA bundle и явно указанная директива openssl.cafile.</p>`,
'о-tilix-и-d-интервью-с-геральдом-нанном': `
<h2>Что вынести из интервью</h2>
<p>Главный практический вывод прост: D может быть не только системным языком, но и удобным языком для прикладных desktop-инструментов. Tilix показывает, что связка D, GtkD и аккуратного следования HIG способна дать зрелое приложение, которым пользуются каждый день.</p>
<p>Если хочется повторить такой путь, начинайте не с большого терминала, а с маленькой GTK-утилиты: окно, настройки, несколько действий, сборка через DUB. Так быстрее станет понятно, насколько привычки из Java, Delphi или PHP мешают или помогают писать на D.</p>`,
'пишем-аналог-функции-php-preg_match_all-на-языке-прог': `
<h2>Как довести класс до рабочего состояния</h2>
<p>После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.</p>
<pre><code>assert(matchAll(r"(\w+)=(\d+)", "a=1 b=22").length == 2);
assert(matchAll(r"z+", "abc").empty);</code></pre>
<p>Дальше можно расширять API постепенно: сначала PREG_PATTERN_ORDER, затем PREG_SET_ORDER, и только после этого режим с OFFSET_CAPTURE.</p>`,
'bitrix-api-создание-добавление-торгового-пре': `
<h2>Что проверить после создания предложения</h2>
<p>После добавления торгового предложения проверьте три связи: привязку к товару, цены и остатки. В Bitrix предложение может успешно создаться, но не появиться в публичной части, если не заполнены обязательные свойства SKU или не обновлены индексы.</p>
<ul>
<li>Убедитесь, что установлен CML2_LINK или соответствующее свойство связи.</li>
<li>После записи цены вызовите обновление элемента и очистите кеш каталога.</li>
<li>Если используются склады, отдельно сохраните количество через API складского учёта.</li>
</ul>
<p>Такой порядок закрывает типичную проблему: элемент есть в админке, но покупатель его не видит.</p>`,
'bitrix-api-функция-для-генерации-кода-элемент': `
<h2>Как использовать функцию безопасно</h2>
<p>Транслитерация имени удобна, но код элемента должен оставаться уникальным. Поэтому после генерации проверяйте существующие символьные коды в инфоблоке и при совпадении добавляйте числовой суффикс.</p>
<pre><code>telefon-samsung
telefon-samsung-2
telefon-samsung-3</code></pre>
<p>Такой простой контроль избавляет от случайной перезаписи URL и от ситуаций, когда два товара получают один и тот же человекочитаемый адрес.</p>`,
'bitrix-api-add-foto-editor': `
<h2>Завершение интеграции</h2>
<p>После вывода компонента важно не забыть обработать результат формы: сохранить ID файла, проверить тип загруженного файла и права пользователя. Сам редактор решает задачу интерфейса, но безопасность и привязка к сущности остаются на стороне вашего кода.</p>
<p>Если редактор не открывается, проверьте подключение модулей main и fileman, права на загрузку и наличие административных JS-расширений на странице. После этого компонент работает стабильно и может использоваться не только в админке, но и в собственных формах.</p>`,
'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': `
<h2>Практический вывод</h2>
<p>Статья важна не только для разработчиков компилятора. Она показывает, почему аллокатор и сборщик мусора нельзя оценивать одной фразой «GC медленный». Нужно смотреть на классы размеров, поиск пулов, стоимость маркировки и то, сколько работы выполняется во время остановки мира.</p>
<p>Для прикладного D-кода вывод такой: держите горячие циклы предсказуемыми, не создавайте лишние временные объекты в tight-loop и измеряйте паузы. Если участок действительно критичен, D позволяет локально отключить GC или заменить аллокации заранее подготовленными буферами.</p>`,
'firefox-увеличение-ожидания-загрузки-timeout-стр': `
<h2>Когда лучше не увеличивать timeout</h2>
<p>Увеличение ожидания помогает при разовых административных операциях, но не должно маскировать медленный сайт. Если обычная страница требует минуты, правильнее вынести задачу в фон: очередь, cron, прогресс-бар и отдельная проверка статуса.</p>
<p>Для phpMyAdmin, импорта и парсинга больших файлов увеличение timeout допустимо. Для публичного пользовательского сценария лучше исправлять серверную часть, иначе браузер станет ждать дольше, но пользователь всё равно получит плохой интерфейс.</p>`,
};
const articleAdditions = {
...additions,
'вирус-самопроизвольный-запуск-брауз': `
<h2>Проверяем, что проблема решена</h2>
<p>После удаления задания перезагрузите компьютер и оставьте систему включённой на тот промежуток времени, через который раньше открывался браузер. Если окно больше не появляется, значит источник найден правильно. Если запуск повторяется, проверяем не только библиотеку планировщика, но и автозагрузку, ключи Run в реестре и расширения браузера.</p>
<p>В планировщике заданий важно смотреть не только название задания. Название злоумышленник может сделать вполне безобидным: Update Service, Browser Check, Windows Helper. Смысл находится в действии. Если в действии указан путь к браузеру, а после него стоит адрес сайта, то перед нами как раз тот самый случай.</p>
<ul>
<li>Откройте свойства подозрительного задания и посмотрите вкладку «Действия».</li>
<li>Проверьте путь к программе: firefox.exe, chrome.exe, browser.exe или cmd.exe с последующим запуском браузера.</li>
<li>Проверьте аргументы: если там указан URL, рекламный домен или короткая ссылка, задание можно отключать.</li>
<li>Сначала отключите задание, перезагрузите компьютер и проверьте поведение. После проверки его уже можно удалить.</li>
</ul>
<p>Если задача появляется снова, значит в системе остался установщик или служба, которая её восстанавливает. В таком случае дополнительно смотрим «Приложения и возможности», папки автозагрузки пользователя, ключи <em>HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run</em> и <em>HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run</em>. После очистки нужно обновить антивирусные базы и выполнить полную проверку.</p>
<p>Итог простой: в этом случае лечится не браузер, а причина его запуска. Находим периодическое задание, проверяем действие, отключаем, убеждаемся, что оно не восстановилось, и только после этого удаляем. Так проблема закрывается полностью, а не до следующей перезагрузки.</p>`,
'использование-jquery-в-webpack': `
<h2>Итоговая схема подключения</h2>
<p>Для старого проекта jQuery в Webpack лучше подключать явно: установить пакет, импортировать его в точке входа и отдельно решить вопрос с глобальными переменными. ProvidePlugin удобен, когда в модулях встречаются свободные идентификаторы <em>$</em> или <em>jQuery</em>. Но если старый плагин лезет именно в <em>window.jQuery</em>, одного ProvidePlugin может быть мало — нужно положить jQuery в <em>window</em> самостоятельно.</p>
<pre><code>import $ from 'jquery';
window.$ = $;
window.jQuery = $;</code></pre>
<p>После этого можно добавить ProvidePlugin, чтобы не писать импорт в каждом файле:</p>
<pre><code>const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
],
};</code></pre>
<p>Порядок подключения имеет значение. Сначала в entry-файле импортируем jQuery и кладём его в <em>window</em>, потом импортируем старые плагины, которым нужен глобальный объект. Если плагин подключается через <em>require</em>, делаем это после установки <em>window.jQuery</em>.</p>
<pre><code>import './jquery-global';
import 'inputmask/dist/jquery.inputmask';
import './app';</code></pre>
<p>Проверка простая: в браузерной консоли должны существовать <em>window.$</em> и <em>window.jQuery</em>, а в собранном bundle не должно быть двух разных копий jQuery. Если проект новый, лучше не тащить jQuery без необходимости. Если проект старый и плагины уже написаны под jQuery, такая схема делает зависимость явной и предсказуемой.</p>`,
'компиляция-64-x-разрядных-программ-на-dmd-по': `
<h2>Короткая памятка для современных и старых сборок</h2>
<p>Главная мысль остаётся той же: нельзя смешивать 32-битные OMF-библиотеки и 64-битную сборку. В старых установках DMD под Windows это часто упиралось в ручную настройку <em>sc.ini</em>, Windows SDK и Visual Studio linker. В более новых установках официальный установщик DMD обычно делает большую часть этой настройки сам, а для нормальной работы достаточно иметь Visual Studio Build Tools или установленную Visual Studio с C++ tooling.</p>
<p>Для простой 64-битной сборки используем явную архитектуру:</p>
<pre><code>dmd -m64 main.d -of=app.exe
dub build --arch=x86_64</code></pre>
<p>Если используется старая 32-битная сборка, смотрим, какой формат объектных файлов нужен проекту. Старый OMF и MS-COFF несовместимы между собой. Для современных библиотек Windows чаще нужен MS-COFF, а для 64-битной сборки это фактически обычный путь.</p>
<ul>
<li>Проверьте, что внешние <em>.lib</em> файлы собраны под ту же архитектуру.</li>
<li>Запускайте сборку из Developer Command Prompt или x64 Native Tools Command Prompt, если компоновщик не находится.</li>
<li>Если DMD не видит linker, проверьте переменные окружения и секцию <em>Environment64</em> в <em>sc.ini</em>.</li>
<li>Если проект собирается через DUB, фиксируйте архитектуру в команде или конфигурации сборки.</li>
</ul>
<p>Типичная ошибка выглядит так: код компилируется, а линковка падает на внешней библиотеке. В большинстве случаев виноват не D-код, а то, что рядом лежит библиотека другого формата или другой разрядности. Сначала проверяем toolchain и зависимости, и только потом ищем ошибку в исходниках.</p>`,
'новый-движок-ctfe': `
<h2>Как применять выводы на практике</h2>
<p>CTFE полезен там, где результат можно посчитать один раз во время компиляции: таблицы констант, парсинг небольших DSL, генерация однотипного кода, предварительная подготовка строк и проверка инвариантов. Но не стоит превращать компиляцию в полноценный runtime. Чем проще входные данные и чем меньше побочных эффектов, тем легче будет сопровождать проект.</p>
<pre><code>enum table = buildLookupTable();
static assert(table.length == 256);</code></pre>
<p>Хороший кандидат для CTFE — функция, которая зависит только от литералов, типов и конфигурации сборки. Плохой кандидат — код, которому нужен ввод-вывод, сеть, текущее время, состояние окружения или большая внешняя база данных. Такой код лучше оставить в runtime или вынести в отдельный генератор, который запускается до сборки.</p>
<p>Для контроля качества CTFE-кода полезно держать рядом <em>static assert</em>. Он сразу показывает, что вычисление действительно произошло на этапе компиляции и вернуло ожидаемый результат. Если compile-time функция стала слишком сложной, её стоит разбить на маленькие чистые функции: так компилятору проще, и человеку проще понять, что происходит.</p>
<p>Практический вывод такой: CTFE в D — мощный инструмент, но сила его не в магии, а в переносе предсказуемых вычислений из runtime в compile-time. Используем его для констант, генерации и проверок. Не используем его как замену обычной программе.</p>`,
'особенности-vibe-d': `
<h2>Когда выбирать vibe.d</h2>
<p>vibe.d хорошо подходит для небольших и средних веб-сервисов на D, где важны неблокирующий ввод-вывод, простая маршрутизация и единая экосистема DUB. Начать можно с минимального HTTP-сервера, затем добавить Diet-шаблоны, MongoDB или Redis и вынести повторяющиеся части в отдельные модули.</p>
<pre><code>shared static this()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
listenHTTP(settings, &handleRequest);
}</code></pre>
<p>При проектировании сервиса главное не забывать, что удобный синтаксис не отменяет асинхронную модель. Долгие CPU-задачи нельзя выполнять прямо в обработчике запроса, иначе они будут мешать обработке ввода-вывода. Для таких задач лучше использовать отдельные worker-потоки, очередь или заранее подготовленные данные.</p>
<ul>
<li>HTTP и WebSocket оставляем в event loop vibe.d.</li>
<li>Тяжёлые вычисления выносим из обработчика запроса.</li>
<li>Доступ к базе оборачиваем в небольшой слой, чтобы не размазывать запросы по маршрутам.</li>
<li>Diet-шаблоны держим простыми: шаблон должен отображать данные, а не выполнять бизнес-логику.</li>
</ul>
<p>Если проект требует большого количества готовых интеграций, заранее проверьте наличие пакетов в DUB и состояние нужных драйверов. Если же нужна компактная серверная часть на D, vibe.d остаётся прямым и понятным выбором: один язык, быстрый нативный код, нормальная маршрутизация, шаблоны и асинхронный ввод-вывод.</p>`,
'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': `
<h2>Финальная проверка настройки</h2>
<p>После подключения файла сертификатов перезапустите веб-сервер или PHP-FPM и выполните тестовый HTTPS-запрос из того же окружения, где возникала ошибка. Важно проверять не браузер, а именно PHP: CLI и веб-сервер могут использовать разные <em>php.ini</em>.</p>
<pre><code>php --ini
php -i | grep -E "curl.cainfo|openssl.cafile"
php -r "var_dump(file_get_contents('https://example.com') !== false);"</code></pre>
<p>Для cURL указывается <em>curl.cainfo</em>, для потоков OpenSSL — <em>openssl.cafile</em>. На Windows путь лучше писать полностью и без относительных директорий. Если используется не глобальный <em>php.ini</em>, а отдельная настройка клиента, можно передать CA bundle прямо в cURL:</p>
<pre><code>curl_setopt($ch, CURLOPT_CAINFO, 'C:\\php\\extras\\ssl\\cacert.pem');</code></pre>
<p>Не отключайте проверку SSL через <em>CURLOPT_SSL_VERIFYPEER = false</em> как постоянное решение. Это скрывает проблему и делает соединение уязвимым. Такая настройка допустима только для короткой диагностики в локальной среде, и после проверки её нужно вернуть обратно.</p>
<p>Если CA bundle указан верно, но ошибка осталась, проверьте цепочку сертификатов на стороне удалённого сервера. Иногда браузер открывает сайт, потому что умеет достраивать цепочку, а PHP/cURL получает неполную цепочку и честно падает. В таком случае исправлять нужно сертификаты сервера, а не PHP-код.</p>
<p>Правильный итог: актуальный <em>cacert.pem</em>, явно указанные <em>curl.cainfo</em> и <em>openssl.cafile</em>, перезапуск PHP и тест именно из того окружения, где работает приложение.</p>`,
'о-tilix-и-d-интервью-с-геральдом-нанном': `
<h2>Что вынести из интервью</h2>
<p>Главный практический вывод прост: D может быть не только системным языком, но и удобным языком для прикладных desktop-инструментов. Tilix показывает, что связка D, GtkD и аккуратного следования GNOME HIG способна дать зрелое приложение, которым пользуются каждый день.</p>
<p>В интервью хорошо видно, что выбор языка был не религиозным, а практическим. Автору не хотелось писать GUI на C или C++, Python оказался неудобен для крупного статически проверяемого приложения, а D дал знакомую после Java модель, нативную скорость и нормальные GTK-привязки. Это важный критерий выбора технологии: не «самый модный язык», а язык, на котором конкретный автор может быстро и спокойно делать продукт.</p>
<p>Если хочется повторить такой путь, начинать стоит не с большого терминала, а с маленькой GTK-утилиты: окно, настройки, несколько действий, сборка через DUB, упаковка и обновления. На таком проекте сразу проявятся реальные вопросы: работа с GtkD, структура проекта, взаимодействие с C-библиотеками, обработка событий и паузы GC.</p>
<ul>
<li>Для GUI-кода держите бизнес-логику отдельно от виджетов.</li>
<li>Не бойтесь GC, но измеряйте горячие участки и большие циклы.</li>
<li>Если нужен C API, сначала сделайте маленький прототип вызова.</li>
<li>Сначала исправляйте ошибки, потом добавляйте функции — именно так Tilix сохранил управляемый backlog.</li>
</ul>
<p>Именно поэтому интервью полезно не только поклонникам D. Оно показывает нормальный инженерный путь: выбрать задачу, подобрать инструмент, сделать небольшую работающую версию, а затем постепенно доводить приложение до зрелого состояния.</p>`,
'пишем-аналог-функции-php-preg_match_all-на-языке-прог': `
<h2>Как довести класс до рабочего состояния</h2>
<p>После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.</p>
<pre><code>assert(matchAll(r"(\w+)=(\d+)", "a=1 b=22").length == 2);
assert(matchAll(r"z+", "abc").empty);</code></pre>
<p>В D стоит явно решить, какой формат результата нужен. PHP поддерживает несколько режимов: <em>PREG_PATTERN_ORDER</em>, <em>PREG_SET_ORDER</em> и вариант с <em>PREG_OFFSET_CAPTURE</em>. Если пытаться сделать всё сразу, код быстро станет запутанным. Лучше сначала реализовать простой режим, затем режим группировки по совпадениям, и только после этого добавить позиции.</p>
<pre><code>struct MatchPart
{
string text;
ptrdiff_t offset;
}
alias MatchSet = MatchPart[][];</code></pre>
<p>Отдельно проверьте Unicode. Если шаблон и строка содержат кириллицу, важно понимать, что именно возвращается в offset: позиция в байтах или позиция в символах. PHP для <em>PREG_OFFSET_CAPTURE</em> возвращает байтовое смещение. Если в D вы хотите повторить поведение PHP, нужно документировать именно байтовый offset, иначе результаты будут отличаться.</p>
<p>Финальный класс должен иметь маленькую поверхность API: метод без offset, метод с offset и понятный enum для порядка результата. Тогда аналог <em>preg_match_all</em> будет не просто копией PHP-функции, а удобным D-инструментом, который предсказуемо работает в тестах.</p>`,
'bitrix-api-создание-добавление-торгового-пре': `
<h2>Что проверить после создания предложения</h2>
<p>После добавления торгового предложения проверьте три связи: привязку к товару, цены и остатки. В Bitrix предложение может успешно создаться, но не появиться в публичной части, если не заполнены обязательные свойства SKU, не сохранён товарный каталог или не обновлены кеши.</p>
<ul>
<li>Убедитесь, что модуль <em>iblock</em> подключён перед созданием элемента.</li>
<li>Если задаются цена и складской остаток, подключите модуль <em>catalog</em>.</li>
<li>Проверьте свойство связи с товаром, чаще всего это <em>CML2_LINK</em> или настроенное в конкретном каталоге свойство SKU.</li>
<li>После записи цены и остатков очистите кеш компонента каталога.</li>
</ul>
<p>Минимальная последовательность такая: создать элемент торгового предложения в инфоблоке SKU, записать связь с товаром, сохранить параметры товара, установить цену, установить количество. Если хотя бы один шаг пропущен, предложение может быть видно в админке, но не попадёт в публичный каталог.</p>
<pre><code>CModule::IncludeModule('iblock');
CModule::IncludeModule('catalog');
$offerId = $el->Add($fields);
CCatalogProduct::Add([
'ID' => $offerId,
'QUANTITY' => 10,
]);
CPrice::SetBasePrice($offerId, 990, 'RUB');</code></pre>
<p>На боевом проекте обязательно логируйте результат <em>Add</em> и текст ошибки <em>LAST_ERROR</em>. Bitrix часто молча возвращает <em>false</em>, а настоящая причина находится именно там: обязательное поле, неправильный инфоблок, неверный код свойства или недостаточные права.</p>`,
'bitrix-api-функция-для-генерации-кода-элемент': `
<h2>Как использовать функцию безопасно</h2>
<p>Транслитерация имени удобна, но код элемента должен оставаться уникальным. Поэтому после генерации проверяйте существующие символьные коды в инфоблоке и при совпадении добавляйте числовой суффикс. Иначе два товара с похожим названием могут получить один URL.</p>
<pre><code>telefon-samsung
telefon-samsung-2
telefon-samsung-3</code></pre>
<p>Также стоит сразу нормализовать результат: привести к нижнему регистру, заменить пробелы на дефис, убрать повторяющиеся дефисы и обрезать дефис в начале или конце строки. Это мелочь, но именно такие мелочи потом портят адреса страниц.</p>
<pre><code>$code = CUtil::translit($name, 'ru', [
'replace_space' => '-',
'replace_other' => '-',
'change_case' => 'L',
]);
$code = trim(preg_replace('/-+/', '-', $code), '-');</code></pre>
<p>Проверку уникальности лучше делать в том же инфоблоке, где будет создан элемент. Если сайт многоязычный или есть разделы с одинаковыми товарами, заранее решите правило: уникальность на весь инфоблок или только внутри раздела. Для SEO обычно проще и надёжнее уникальность на весь инфоблок.</p>
<p>В итоге функция должна не просто транслитерировать строку, а возвращать готовый символьный код: чистый, нижнего регистра, без мусора и без совпадений с уже существующими элементами.</p>`,
'bitrix-api-add-foto-editor': `
<h2>Завершение интеграции</h2>
<p>После вывода компонента важно не забыть обработать результат формы: сохранить ID файла, проверить тип загруженного файла и права пользователя. Сам редактор решает задачу интерфейса, но безопасность и привязка к сущности остаются на стороне вашего кода.</p>
<p>В параметрах компонента отдельно проверьте <em>allowUpload</em>, <em>medialib</em>, <em>fileDialog</em>, <em>cloud</em>, <em>delete</em>, <em>edit</em> и <em>maxCount</em>. Не все параметры будут иметь эффект в любой установке Bitrix: часть зависит от подключённых модулей, прав пользователя и контекста страницы.</p>
<pre><code>$fileId = (int)$_POST['picture'];
if ($fileId > 0) {
$file = CFile::GetFileArray($fileId);
if ($file && str_starts_with($file['CONTENT_TYPE'], 'image/')) {
// сохраняем ID картинки в своей сущности
}
}</code></pre>
<p>Если редактор не открывается, проверьте подключение модулей <em>main</em> и <em>fileman</em>, права на загрузку, административные JS-расширения и наличие сессии пользователя. В публичной части сайта проблема часто оказывается не в <em>FileInput</em>, а в том, что на странице не подключены нужные скрипты Bitrix.</p>
<p>Рабочая схема такая: выводим FileInput, разрешаем только картинки, ограничиваем количество, после отправки формы валидируем ID файла и сохраняем его в свою сущность. Тогда встроенный редактор становится нормальной частью формы, а не просто красивой кнопкой загрузки.</p>`,
'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': `
<h2>Практический вывод</h2>
<p>Статья важна не только для разработчиков компилятора. Она показывает, почему аллокатор и сборщик мусора нельзя оценивать одной фразой «GC медленный». Нужно смотреть на классы размеров, поиск пулов, стоимость маркировки и то, сколько работы выполняется во время остановки мира.</p>
<p>Для прикладного D-кода вывод такой: держите горячие циклы предсказуемыми, не создавайте лишние временные объекты в tight-loop и измеряйте паузы. Если участок действительно критичен, D позволяет локально управлять GC и заменить поток мелких аллокаций заранее подготовленными буферами.</p>
<pre><code>import core.memory : GC;
GC.disable();
scope(exit) GC.enable();
// горячий участок без лишних временных объектов</code></pre>
<p>Отключать GC нужно аккуратно. Это не волшебная оптимизация, а договор: пока сборщик выключен, программа не должна бесконтрольно накапливать мусор. Поэтому такой приём подходит для короткого измеримого участка, а не для всего приложения.</p>
<ul>
<li>Сначала измеряем: время, количество аллокаций, паузы.</li>
<li>Потом убираем лишние временные объекты и повторные выделения.</li>
<li>Только затем используем ручное управление GC, если оно действительно помогает.</li>
<li>После оптимизации оставляем комментарий, почему здесь нельзя писать «просто как обычно».</li>
</ul>
<p>Главный смысл доклада в том, что производительность памяти — это архитектура. Если в программе много мелких объектов, непредсказуемых ссылок и больших пауз, проблема редко решается одной настройкой. Нужно менять модель данных, жизненный цикл объектов и место, где выделяется память.</p>`,
'firefox-увеличение-ожидания-загрузки-timeout-стр': `
<h2>Когда лучше не увеличивать timeout</h2>
<p>Увеличение ожидания помогает при разовых административных операциях, но не должно маскировать медленный сайт. Если обычная страница требует минуты, правильнее вынести задачу в фон: очередь, cron, прогресс-бар и отдельная проверка статуса.</p>
<p>Важно не путать разные таймауты. Ожидание HTTP-ответа — это одно, долго выполняющийся JavaScript на странице — другое, а таймауты прокси или веб-сервера — третье. Если Firefox ждёт дольше, но nginx, Apache, PHP-FPM или upstream уже оборвал соединение, настройка браузера не поможет.</p>
<ul>
<li>Для долгого ответа сервера проверяйте HTTP timeout в браузере и серверные таймауты.</li>
<li>Для долгого JavaScript смотрите предупреждения о зависшем скрипте и настройки выполнения JS.</li>
<li>Для импорта больших файлов лучше использовать фоновую задачу и страницу прогресса.</li>
<li>Для phpMyAdmin и разовых административных операций увеличение ожидания допустимо.</li>
</ul>
<p>Если параметра в <em>about:config</em> нет, его можно создать вручную только тогда, когда вы понимаете, какая версия Firefox и какой именно timeout вам нужен. После завершения работы лучше вернуть настройку к обычному значению, чтобы браузер не висел слишком долго на реально недоступных сайтах.</p>
<p>Итог: увеличивать timeout можно как временный инструмент администратора. Для пользовательского сценария правильнее исправлять серверную часть, потому что долгий белый экран остаётся плохим интерфейсом даже тогда, когда браузер согласен ждать.</p>`,
};
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
function decodeHtml(value = '') {
const named = { amp: '&', apos: "'", gt: '>', hellip: '...', laquo: '«', lt: '<', mdash: '—', nbsp: ' ', quot: '"', raquo: '»' };
return value
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10)))
.replace(/&([a-z]+);/gi, (_, name) => named[name] ?? `&${name};`);
}
function stripTags(value = '') {
return decodeHtml(value.replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim();
}
function normalizeContent(html) {
let content = html
.replace(/<span id="more-\d+"><\/span>/g, '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '');
content = content.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => {
const text = decodeHtml(code.replace(/<[^>]+>/g, ''));
return `<pre><code>${escapeHtml(text.trim())}</code></pre>`;
});
content = content.replace(/<img\b[^>]*>/gi, (tag) => {
const src = decodeHtml(tag.match(/\bsrc=["']([^"']+)["']/i)?.[1] ?? '');
const found = imageMap.find(([needle]) => src.includes(needle));
if (!found) return '';
const [, imageSrc, defaultAlt] = found;
const originalAlt = decodeHtml(tag.match(/\balt=["']([^"']*)["']/i)?.[1] ?? '');
const alt = originalAlt || defaultAlt;
return `<figure class="article-media"><img src="${imageSrc}" alt="${escapeHtml(alt)}" /><figcaption>${escapeHtml(defaultAlt)}</figcaption></figure>`;
});
content = content
.replace(/\s+(srcset|sizes|width|height|class)=["'][^"']*["']/gi, '')
.replace(/<p>\s*<\/p>/g, '')
.replace(/\n{3,}/g, '\n\n')
.trim();
return content;
}
function text(x, y, body, options = {}) {
const attrs = [
`x="${x}"`,
`y="${y}"`,
options.size ? `font-size="${options.size}"` : '',
options.weight ? `font-weight="${options.weight}"` : '',
options.fill ? `fill="${options.fill}"` : '',
options.family ? `font-family="${options.family}"` : '',
].filter(Boolean).join(' ');
return `<text ${attrs}>${escapeHtml(body)}</text>`;
}
function rect(x, y, width, height, fill, stroke = '#263241', radius = 10) {
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" fill="${fill}" stroke="${stroke}" stroke-width="2"/>`;
}
function baseSvg(width, height, title, subtitle, accent, body) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(title)}">
<rect width="${width}" height="${height}" fill="#f7f4ed"/>
<rect x="24" y="24" width="${width - 48}" height="${height - 48}" rx="18" fill="#ffffff" stroke="#242424" stroke-width="2"/>
<rect x="24" y="24" width="${width - 48}" height="14" rx="7" fill="${accent}"/>
${text(56, 82, title, { size: 30, weight: 700, fill: '#1f2933' })}
${subtitle ? text(56, 116, subtitle, { size: 16, fill: '#52606d' }) : ''}
${body}
</svg>`;
}
function diagramAsset(title, subtitle, accent, labels) {
const items = labels.map((item, index) => {
const x = 64 + index * 178;
const arrow = index < labels.length - 1 ? `<path d="M${x + 132} 248h44" stroke="#687789" stroke-width="4" stroke-linecap="round"/><path d="M${x + 168} 238l16 10-16 10" fill="none" stroke="#687789" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>` : '';
return `${rect(x, 190, 130, 96, '#f8fafc', '#d1d8e0', 12)}${text(x + 20, 229, item[0], { size: 17, weight: 700, fill: '#1f2933' })}${text(x + 20, 258, item[1], { size: 13, fill: '#52606d' })}${arrow}`;
}).join('');
return baseSvg(860, 420, title, subtitle, accent, items);
}
function codeAsset(title, subtitle, accent, lines) {
const code = lines.map((line, index) => text(84, 178 + index * 28, line, { size: 17, fill: '#d7e3f5', family: 'SFMono-Regular, Menlo, Consolas, monospace' })).join('');
return baseSvg(860, 460, title, subtitle, accent, `
${rect(60, 140, 740, 248, '#111827', '#111827', 14)}
<circle cx="92" cy="164" r="6" fill="#ef4444"/><circle cx="114" cy="164" r="6" fill="#f59e0b"/><circle cx="136" cy="164" r="6" fill="#22c55e"/>
${code}
`);
}
function featureCard(title, subtitle, accent, rows) {
const body = rows.map((row, index) => {
const y = 168 + index * 58;
return `<circle cx="90" cy="${y - 8}" r="14" fill="${accent}"/><path d="M83 ${y - 8}l5 6 11-14" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>${text(124, y, row, { size: 20, fill: '#263241' })}`;
}).join('');
return baseSvg(760, 420, title, subtitle, accent, body);
}
function taskScheduler(kind) {
const isAction = kind === 'action';
return baseSvg(980, 520, isAction ? 'Планировщик заданий: действие' : 'Запуск планировщика заданий', isAction ? 'В аргументах браузера виден лишний URL' : 'Win + R, затем taskschd.msc', '#2f6fed', `
${rect(70, 148, 840, 300, '#eef3fb', '#bfccdc', 8)}
${text(96, 186, isAction ? 'Библиотека планировщика заданий' : 'Выполнить', { size: 22, weight: 700, fill: '#1f2933' })}
${isAction ? `
${rect(96, 212, 300, 192, '#fff', '#d1d8e0', 6)}
${text(118, 248, 'Update Service', { size: 17, fill: '#1f2933' })}
${text(118, 282, 'Browser Updater', { size: 17, fill: '#1f2933' })}
${text(118, 316, 'Mozilla Firefox Task', { size: 17, fill: '#b42318', weight: 700 })}
${rect(430, 212, 420, 192, '#fff', '#d1d8e0', 6)}
${text(456, 250, 'Программа:', { size: 16, fill: '#52606d' })}
${text(456, 282, 'firefox.exe', { size: 20, fill: '#1f2933', family: 'monospace' })}
${text(456, 326, 'Аргументы:', { size: 16, fill: '#52606d' })}
${text(456, 358, 'http://example-bad-site.test', { size: 20, fill: '#b42318', family: 'monospace' })}
` : `
${text(110, 245, 'Открыть:', { size: 18, fill: '#52606d' })}
${rect(110, 268, 540, 54, '#fff', '#9aa8b8', 6)}
${text(132, 303, 'taskschd.msc', { size: 24, fill: '#1f2933', family: 'monospace' })}
${rect(670, 268, 128, 54, '#2f6fed', '#2f6fed', 6)}
${text(704, 303, 'OK', { size: 22, fill: '#fff', weight: 700 })}
${text(110, 372, 'Так открывается системный инструмент, где видны все периодические запуски.', { size: 17, fill: '#52606d' })}
`}
`);
}
function tilixScreenshot() {
return baseSvg(1040, 600, 'Tilix', 'Тайлинговый терминал с боковой панелью сессий', '#7c3aed', `
${rect(66, 130, 908, 390, '#1f2933', '#111827', 12)}
<rect x="66" y="130" width="908" height="44" rx="12" fill="#343b46"/>
${text(100, 159, 'project: ~/src/tilix', { size: 16, fill: '#d7e3f5', family: 'monospace' })}
${rect(88, 198, 170, 286, '#111827', '#334155', 8)}
${text(112, 232, 'sessions', { size: 15, fill: '#94a3b8', family: 'monospace' })}
${text(112, 272, 'build', { size: 18, fill: '#f8fafc', family: 'monospace' })}
${text(112, 310, 'ssh prod', { size: 18, fill: '#f8fafc', family: 'monospace' })}
${text(112, 348, 'logs', { size: 18, fill: '#f8fafc', family: 'monospace' })}
${rect(286, 198, 308, 132, '#0f172a', '#475569', 8)}
${rect(616, 198, 308, 132, '#0f172a', '#475569', 8)}
${rect(286, 352, 638, 132, '#0f172a', '#475569', 8)}
${text(316, 238, '$ dub build', { size: 19, fill: '#a7f3d0', family: 'monospace' })}
${text(646, 238, '$ journalctl -f', { size: 19, fill: '#bfdbfe', family: 'monospace' })}
${text(316, 394, '$ grep -R GtkD source/', { size: 19, fill: '#fde68a', family: 'monospace' })}
${text(316, 430, 'ready', { size: 18, fill: '#e5e7eb', family: 'monospace' })}
`);
}
function gcPools() {
return baseSvg(900, 520, 'GC D: малые пулы', 'Классы размеров и страницы памяти', '#0f766e', `
${[16, 32, 64, 128, 256, 512].map((size, i) => {
const y = 150 + i * 48;
return `${text(74, y + 28, `${size} B`, { size: 17, fill: '#1f2933', family: 'monospace' })}${rect(150, y, 620, 32, '#ecfeff', '#99f6e4', 4)}${Array.from({ length: 10 }, (_, j) => `<rect x="${164 + j * 58}" y="${y + 7}" width="${38 + (j % 3) * 5}" height="18" rx="3" fill="${j % 2 ? '#14b8a6' : '#0f766e'}"/>`).join('')}`;
}).join('')}
${text(74, 470, 'Каждая строка показывает отдельный класс размера и фрагментацию внутри страницы.', { size: 16, fill: '#52606d' })}
`);
}
function gcLargePool() {
return baseSvg(900, 520, 'GC D: большие объекты', 'Метаданные растут вместе с диапазоном страниц', '#c2410c', `
${rect(86, 166, 728, 92, '#fff7ed', '#fdba74', 10)}
${text(112, 222, 'object > 100 MB', { size: 25, fill: '#9a3412', family: 'monospace', weight: 700 })}
${rect(86, 310, 728, 58, '#f8fafc', '#d1d8e0', 8)}
${Array.from({ length: 18 }, (_, i) => `<rect x="${108 + i * 38}" y="323" width="24" height="32" rx="4" fill="${i < 11 ? '#fb923c' : '#cbd5e1'}"/>`).join('')}
${text(86, 410, 'Один большой объект может оставить заметный хвост таблиц и свободных страниц.', { size: 17, fill: '#52606d' })}
`);
}
const assetFiles = {
'cover-virus.svg': baseSvg(900, 480, 'Планировщик против рекламного запуска', 'Схема поиска вредного задания', '#2f6fed', `${rect(86, 168, 728, 178, '#eef3fb', '#bfccdc', 12)}${text(128, 230, 'taskschd.msc', { size: 38, weight: 700, fill: '#1f2933', family: 'monospace' })}${text(128, 286, 'firefox.exe + URL', { size: 28, fill: '#b42318', family: 'monospace' })}`),
'taskschd-run.svg': taskScheduler('run'),
'taskschd-action.svg': taskScheduler('action'),
'jquery-webpack.svg': codeAsset('jQuery в Webpack', 'Один экземпляр библиотеки для модулей и старых плагинов', '#2563eb', ['import $ from "jquery";', 'window.$ = window.jQuery = $;', '', 'new webpack.ProvidePlugin({', ' $: "jquery",', ' jQuery: "jquery"', '});']),
'dmd-win64.svg': diagramAsset('DMD x64 под Windows', 'Не смешиваем OMF и COFF библиотеки', '#7c2d12', [['D source', 'main.d'], ['dmd -m64', 'compiler'], ['COFF libs', 'x64'], ['EXE', 'win64']]),
'ctfe-engine.svg': diagramAsset('NewCTFE', 'Вычисления во время компиляции без лишней магии', '#7c3aed', [['AST', 'input'], ['CTFE VM', 'evaluate'], ['Result', 'constant'], ['Binary', 'ready']]),
'vibe-cover.svg': diagramAsset('vibe.d', 'Асинхронный веб-стек на языке D', '#059669', [['HTTP', 'server'], ['Fiber', 'async I/O'], ['Mongo/Redis', 'drivers'], ['Diet', 'views']]),
'vibe-async.svg': featureCard('Асинхронность', 'Задачи выглядят синхронно, I/O остаётся неблокирующим', '#059669', ['fiber на запрос', 'ожидание без блокировки', 'простая модель ошибок']),
'vibe-api.svg': featureCard('Короткий API', 'Минимум кода для типовых веб-задач', '#2563eb', ['маршруты рядом с логикой', 'простые настройки сервера', 'DUB-пакеты']),
'vibe-web.svg': featureCard('Веб-инструменты', 'HTTP, WebSocket, сессии и шаблоны', '#dc2626', ['статические файлы', 'JSON/BSON', 'SMTP и криптография']),
'vibe-db.svg': featureCard('Базы данных', 'MongoDB и Redis доступны из коробки', '#0f766e', ['асинхронные драйверы', 'простые запросы', 'расширение через DUB']),
'vibe-io.svg': featureCard('Неблокирующий I/O', 'libevent и системные API вместо ожидания потока', '#9333ea', ['сетевые операции', 'файловые операции', 'меньше простаивания']),
'vibe-dlang.svg': featureCard('Язык D', 'Скорость нативного кода и выразительность высокого уровня', '#b91c1c', ['типы и шаблоны', 'быстрая компиляция', 'удобная метапрограмма']),
'vibe-diet.svg': featureCard('Diet templates', 'HTML без лишнего синтаксического шума', '#0f172a', ['наследник Pug/Haml', 'динамические вставки', 'читаемые view']),
'php-ssl-cacert.svg': diagramAsset('PHP SSL CA bundle', 'Ошибка лечится сертификатами, а не отключением проверки', '#0f766e', [['PHP', 'request'], ['OpenSSL', 'verify'], ['CA file', 'cacert.pem'], ['HTTPS', 'ok']]),
'tilix-cover.svg': diagramAsset('Tilix + D', 'Desktop-приложение на GtkD', '#7c3aed', [['GtkD', 'UI'], ['D', 'logic'], ['VTE', 'terminal'], ['GNOME', 'HIG']]),
'tilix-screenshot.svg': tilixScreenshot(),
'preg-match-all-d.svg': codeAsset('preg_match_all на D', 'Регулярные выражения, группы и позиции совпадений', '#be123c', ['auto r = regex(r"(\\\\w+)=(\\\\d+)");', 'auto m = matchAll("a=1 b=22", r);', '', 'foreach (hit; m) {', ' writeln(hit.captures);', '}']),
'bitrix-offer-api.svg': codeAsset('Bitrix API: SKU', 'Создание торгового предложения и привязка к товару', '#1d4ed8', ['CModule::IncludeModule("iblock");', '$el = new CIBlockElement;', '$id = $el->Add($fields);', 'CPrice::SetBasePrice($id, 990);']),
'bitrix-translit-api.svg': codeAsset('Bitrix API: translit', 'Символьный код из имени элемента', '#0f766e', ['$code = CUtil::translit($name, "ru", [', ' "replace_space" => "-",', ' "replace_other" => "-",', ']);']),
'bitrix-photo-editor.svg': diagramAsset('Bitrix photo editor', 'Встроенный FileInput для загрузки и редактирования картинки', '#1d4ed8', [['FileInput', 'create'], ['upload', 'image'], ['edit', 'crop'], ['save', 'file ID']]),
'bitrix-photo-editor-ui.svg': baseSvg(1030, 720, 'Редактор картинок Bitrix', 'Читаемый интерфейс FileInput', '#1d4ed8', `${rect(70, 150, 890, 470, '#eef3fb', '#bfccdc', 10)}${rect(110, 200, 520, 360, '#ffffff', '#9aa8b8', 8)}<path d="M160 494l132-150 102 110 70-76 116 116z" fill="#93c5fd"/><circle cx="484" cy="286" r="46" fill="#fde68a"/>${rect(668, 200, 238, 56, '#fff', '#d1d8e0', 8)}${text(690, 236, 'Обрезать', { size: 20, fill: '#1f2933' })}${rect(668, 278, 238, 56, '#fff', '#d1d8e0', 8)}${text(690, 314, 'Повернуть', { size: 20, fill: '#1f2933' })}${rect(668, 356, 238, 56, '#1d4ed8', '#1d4ed8', 8)}${text(710, 392, 'Сохранить', { size: 20, fill: '#fff', weight: 700 })}`),
'dconf-gc-cover.svg': diagramAsset('D GC under the hood', 'Пулы, страницы, маркировка и очистка', '#0f766e', [['Pool', 'memory'], ['Mark', 'scan'], ['Sweep', 'free'], ['Reuse', 'alloc']]),
'gc-small-pools.svg': gcPools(),
'gc-large-pool.svg': gcLargePool(),
'firefox-timeout.svg': baseSvg(860, 420, 'Firefox timeout', 'Когда браузеру нужно ждать дольше', '#ea580c', `${rect(80, 170, 700, 80, '#fff7ed', '#fed7aa', 12)}${text(112, 222, 'network.http.response.timeout = 600', { size: 28, fill: '#9a3412', family: 'monospace', weight: 700 })}${rect(80, 286, 520, 26, '#fed7aa', '#fed7aa', 13)}${rect(80, 286, 390, 26, '#ea580c', '#ea580c', 13)}${text(80, 352, 'Для административных операций, а не для маскировки медленного сайта.', { size: 18, fill: '#52606d' })}`),
};
await mkdir(illustrationsDir, { recursive: true });
await mkdir(mediaDir, { recursive: true });
await mkdir(dataDir, { recursive: true });
for (const [name, svg] of Object.entries(assetFiles)) {
await writeFile(join(illustrationsDir, name), svg);
}
for (const name of ['cropped-WallpapersMania.nnm_.ru_vol141-087.jpg', 'vibe.d.png', 'logo.png', 'bitrix-foto-editor-1.jpg', 'Без-имени.png', '-имени-e1495740674880.png', 'i-880x660.jpg']) {
try {
await copyFile(join(sourceRoot, 'assets', name), join(mediaDir, name));
} catch {
// Generated illustrations are enough for the site; copied media is optional.
}
}
const articles = rawArticles.filter((article) => article.slug !== 'привет-мир').map((article) => {
const content = normalizeContent(article.contentHtml);
const addition = articleAdditions[article.slug]?.trim() ?? '';
const contentHtml = [content, addition].filter(Boolean).join('\n');
return {
slug: article.slug,
title: article.title,
date: article.date,
author: 'DarkRiDDeR',
categories: categories[article.slug] ?? [],
cover: covers[article.slug],
excerpt: article.paragraphs.join(' ').replace(/\s+/g, ' ').slice(0, 260),
contentHtml,
readingMinutes: Math.max(1, Math.round(stripTags(contentHtml).split(/\s+/).length / 180)),
};
}).sort((a, b) => new Date(b.date) - new Date(a.date));
await writeFile(join(dataDir, 'articles.json'), `${JSON.stringify(articles, null, 2)}\n`);
console.log(`Generated ${articles.length} articles and ${Object.keys(assetFiles).length} illustration assets`);