- BrainTools - https://www.braintools.ru -

UML-редактор с блок-схемами по ГОСТ — как устроено внутри

Зачем мне понадобился свой редактор

Я работаю системным аналитиком, и UML-диаграммы для меня это часть работы. Но официальный редактор на plantuml.com из России не открывается, а ставить Java и плагин в IDE или VPN на рабочий компьютер не хочется.

Поэтому я навайбкодил umleditor.ru [1] – редактор UML-диаграмм в браузере. Пишешь код и сразу видишь схему. Регистрироваться не надо. Схемы рисует мой собственный PlantUML-сервер в Москве, так что код диаграмм не уходит на чужие сервисы.

Весь сайт написал Claude: интерфейс, серверную часть, скрипты для сервера, даже тексты справочника. Я объяснял, что хочу, смотрел результат, просил переделать и делал то, что нейросеть сделать не может: платил за сервер, покупал домен и вводил пароли.

Первая рабочая версия появилась за два часа. Потом проект рос: блок-схемы по ГОСТ со своим рендерером, редактор JSON, справочник на 70+ страниц. Ниже описание того, как это устроено внутри, с кодом и цифрами.

Что умеет сайт

Диаграмма последовательности

Диаграмма последовательности
  • 16 видов диаграмм PlantUML: классов, прецедентов, последовательности, деятельности, состояний, ER, Ганта, интеллект-карты и другие.

  • Блок-схемы алгоритмов по ГОСТ 19.701-90: пишешь псевдокод по-русски, схема рисуется сразу.

  • Редактор JSON [2] с деревом и переводом в YAML и XML.

  • Шаблоны, кнопки вставки типовых конструкций, экспорт в PNG и SVG.

  • Диаграммы сохраняются без регистрации.

  • Справочник [3], 20 задач с блок-схемами [4] и готовые примеры [5] – пригодится студентам для курсовых.

Архитектура

Диаграмма развертывания

Диаграмма развертывания

Весь сайт живёт на одном VPS за 525 ₽ в месяц, в трёх контейнерах Docker Compose. Снаружи открыт только Caddy, он сам получает сертификат Let’s Encrypt, сжимает ответы и проксирует всё в приложение. PlantUML-сервер слушает только внутреннюю сеть Docker.

  • Бэкенд – Node.js 24 и Express 5. Это единственная зависимость сервера: SQLite встроен в Node с версии 22.13, а лимиты, кеш, cookie и сжатие PlantUML написаны своим кодом.

  • Фронтенд – Vite и обычные ES-модули без фреймворка, две страницы сборки: редактор диаграмм и редактор JSON. Mermaid подключается только для части видов диаграмм.

  • Общий код. Папка frontend/src/lib – описания диаграмм, справочник, задачи, рендерер ГОСТ, парсеры. Её импортируют и браузер, и сервер.

  • Хранение без регистрации. Посетитель получает случайный uid в cookie, диаграммы лежат в одной таблице с индексом (owner, updated_at DESC), режим WAL.

  • Выкладка. deploy.bat упаковывает проект tar и по SSH распаковывает его на сервере. Скрипт настройки ставит Docker и открывает порты 80 и 443. Если Docker Hub из России не отвечает, он включает зеркало реестра и запускает docker compose up -d --build.

Как рисуется схема PlantUML

Браузер шлёт код на POST /api/render, а сервер сжимает его и передаёт контейнеру plantuml-server. PlantUML принимает диаграмму прямо в URL: текст сжимается deflate и кодируется своим вариантом base64 с другим алфавитом.

import { deflateRawSync } from "node:zlib";

const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";

export function encodePlantUml(text) {
  const data = deflateRawSync(Buffer.from(text, "utf8"), { level: 9 });
  let out = "";
  for (let i = 0; i < data.length; i += 3) {
    const [b1, b2 = 0, b3 = 0] = [data[i], data[i + 1], data[i + 2]];
    out += ALPHABET[b1 >> 2] + ALPHABET[((b1 & 3) << 4) | (b2 >> 4)]
         + ALPHABET[((b2 & 15) << 2) | (b3 >> 6)] + ALPHABET[b3 & 63];
  }
  return out;
}

Короткий код, до 4000 символов после кодирования уходит GET-запросом /svg/<код>, длинный – POST с текстом в теле. На запрос даётся 15 секунд через AbortController, потом понятная ошибка [6] вместо висящего редактора.

Что ещё пришлось сделать вокруг этого вызова:

  • Песочница. Сервер публичный, а PlantUML умеет !include файлов и URL. Контейнер запущен с PLANTUML_SECURITY_PROFILE=SANDBOX, PLANTUML_LIMIT_SIZE=8192, -Xmx512m и mem_limit: 900m, и наружу не открыт: его видит только контейнер приложения.

  • Очистка SVG. Картинка вставляется в страницу как разметка, поэтому сервер вырезает <script> и атрибуты on*= перед отдачей.

  • Кеш. Ключ – SHA-1 от исходника, LRU на 300 записей на Map. Удачные ответы и ответы с ошибкой синтаксиса кешируются, сетевые сбои нет.

  • Лимит. 60 отрисовок в минуту с одного IP: фиксированное окно в памяти [7], ответ 429 с Retry-After. За Caddy включён trust proxy, иначе у всех посетителей был бы один IP прокси и один общий лимит.

  • Номер строки ошибки. PlantUML отдаёт его в заголовке X-PlantUML-Diagram-Error-Line, и он считает с нуля, а в самой картинке ошибки пишет [From string (line 4)] уже с единицы. До нагрузочного теста редактор подсвечивал строку на одну выше настоящей. Теперь номер берётся из картинки, а заголовок – запасной вариант:

export function errorLineOf(body, lineHeader) {
  const m = /(line (d+))/.exec(body || "");
  if (m) return Number(m[1]);                       // в картинке - с единицы
  return lineHeader ? Number(lineHeader) + 1 : null; // в заголовке - с нуля
}

Пока в коде ошибка, редактор показывает последнюю удачную версию схемы, а не красную картинку PlantUML.

Свой рендерер блок-схем по ГОСТ 19.701

Мне этого было достаточно, но Claude подсказал, что люди еще часто ищут блок-схемы. Поэтому я решил и их добавить. Сначала я попросил сделать их на Mermaid, и вышло криво, подписи “да/нет” наезжали на линии, стрелки циклов шли дугами, а пропорции блоков не совпадали с ГОСТом. Общий алгоритм раскладки графа тут мешает. У блок-схемы структура известна заранее. Поэтому рендерер свой: около 400 строк без зависимостей.

На входе псевдокод по-русски, вложенность задаётся отступами, как в Python:

блок-схема: Двоичный поиск
начало
ввод n, массив a[1..n], x
L = 1
R = n
пока L <= R
    m = (L + R) div 2
    если a[m] = x
        вывод m
    иначе если a[m] < x
        L = m + 1
    иначе
        R = m - 1
конец

Разбор. Строки превращаются в дерево узлов term, io, proc, sub, if, while, for, repeat. Рекурсивная функция block(indent) читает строки своего отступа и спускается глубже, когда отступ растёт. Цепочка если/иначе если/иначе становится вложенными ромбами в ветке “нет”. Ошибка всегда несёт номер строки: “Лишний отступ”, “иначе без если”, “После “если” нужно условие”.

Раскладка. Каждый узел сам сообщает, сколько места ему нужно слева и справа от оси потока и какой высоты он будет. Рисует он себя сам, когда родитель даст координаты. Вход всегда сверху, выход снизу на оси:

// каждый узел: { l, r, h, draw(x, y) }
function shape(n) {
  const h = n.t === "term" ? TH : H;
  return {
    l: W / 2, r: W / 2 + commentSize(n).w, h,
    draw(x, y) { /* прямоугольник, параллелограмм или "таблетка" + текст */ },
  };
}

Последовательность складывает высоты детей и берёт максимум их l и r. Ветвление ставит ветку “да” слева, а “нет” справа на расстоянии, чтобы их r и l не пересеклись. Цикл добавляет коридор слева под возвратную линию и справа под выход. Один проход снизу вверх считает размеры, один сверху вниз рисует.

Правила ГОСТа в коде:

  • Блок в пропорции b = 1,5a, ширина одна на всю схему.

  • Ромб с длинным условием расширяется ступенями 1 – 1,25 – 1,5 ширины, пока текст не влезет в две строки. Высота считается из геометрии ромба: чтобы прямоугольник текста поместился, нужно условие h ≥ th / (1 − tw/w).

  • Цикл “для” – шестиугольник “модификация”, вызов – прямоугольник с двойными боками, комментарий – квадратная скобка на пунктире.

  • Линии только горизонтальные и вертикальные. Стрелка ставится только там, где поток идёт не сверху вниз и не слева направо: на возвратах циклов и слияниях справа налево.

  • * выводится как ·, <= как ≤, != как ≠.

Без DOM. Рендерер собирает SVG строкой и не трогает document, поэтому один и тот же файл работает в браузере и на сервере, где из него делаются картинки для страниц задач. Ширина текста без measureText оценивается таблицей ширин символов в долях кегля:

function textWidth(s, fs = FS) {
  let w = 0;
  for (const ch of s) {
    if (ch === " ") w += 0.28;
    else if ("ijlI.,:;|!'()[]".includes(ch)) w += 0.3;
    else if ("mwMWШЩЖЮМЫФ".includes(ch)) w += 0.85;
    else if (/[A-ZА-ЯЁ]/.test(ch)) w += 0.68;
    else w += 0.56;
  }
  return w * fs;
}

Погрешность такой оценки несколько процентов, и она закрывается полями внутри блоков. Шрифт – ГОСТ тип B, если он есть в системе, иначе Arial. Все 20 задач сайта прогоняются через рендерер в тестах.

Блок-схема двоичного поиска

Блок-схема двоичного поиска

Редактор JSON, YAML и XML без библиотек

JSON.parse тут не подошёл по трём причинам:

  1. Объект JavaScript переставляет целочисленные ключи в начало: {"2":1,"1":2} превращается в {"1":2,"2":1}.

  2. 2.50 становится 2.5, а большие целые теряют точность.

  3. Сообщение об ошибке зависит от браузера и обычно сводится к Unexpected token.

Поэтому все три формата разбираются своим кодом в общее дерево, где порядок ключей и запись чисел сохраняются:

{ t: "obj", e: [["заказ", { t: "num", v: "10452", ln: 2 }], ...] } // объект: массив пар
{ t: "arr", a: [...] }
{ t: "str", v } | { t: "num", v: "2.50" } | { t: "bool", v } | { t: "null" }

У каждого узла есть ln – строка в исходнике. Щелчок по узлу дерева переносит курсор к нужной строке кода. Изменение в дереве переписывает текст в текущем формате, а переключатель JSON/YAML/XML – это просто запись того же дерева другим сериализатором.

YAML. Полная спецификация огромна, поэтому разбирается то, что встречается в конфигах:

  • вложенные словари и списки, в том числе - ключ: значение;

  • записи в скобках [ ] и { }, многострочные;

  • строки в кавычках, блоки | и > с модификаторами -, + и цифрой отступа;

  • комментарии.

Якоря &/*, слияние << и несколько документов в файле честно сообщают “не поддерживается” с номером строки, а не разбираются неверно.

Парсер сверялся с PyYAML: один и тот же текст разбирали оба, и JSON на выходе должен был совпасть. Так нашлась ошибка в блоке >: пустая строка давала nn вместо n.

Обратная запись опаснее разбора. Строки, которые другой парсер прочитает не как строку, нужно брать в кавычки:

  • "yes", "no", "y" – в YAML 1.1 это логические значения;

  • "2026-10-06" – PyYAML превращает в дату;

  • "10:00" – в YAML 1.1 это число 600 в шестидесятеричной записи;

  • "0123" – иначе потеряется ведущий ноль.

Даты и времена попали в этот список только после теста на примере заказа с полем дата. В тестах прогоняются кругом JSON – YAML – JSON три десятка “скользких” строк.

XML переводится по привычному соглашению:

  • атрибуты становятся ключами @имя;

  • текст рядом с тегами становится ключом #text;

  • повторяющиеся теги собираются в массив.

Парсер XML тоже свой, без DOMParser, чтобы тот же код работал на сервере и сообщал строку ошибки: “Закрывающий тег </b> не совпадает с открытым <a>”.

Большие файлы. На JSON в 4,3 МБ каждое действие на обычном компьютере занимало 3-4 секунды. Профилировщик показал, что разбор занимает ~150 мс, а всё остальное это чтение textarea.scrollTop. Оно заставляло браузер заново верстать текст вместе с колонкой из 240 тысяч номеров строк. Теперь номера рисуются только для видимых строк, а подсветка синтаксиса выключается на текстах длиннее 200 тысяч символов. В облачном браузере без видеокарты, где всё медленнее, одно действие стало занимать 7 секунд вместо 11. Остаток это вёрстка самого textarea, и дальше поможет только виртуальный редактор вроде CodeMirror.

Справочник: серверные страницы и переходы без перезагрузки

Редактор – одностраничное приложение, и поисковики видят в нём пустую страницу. Поэтому справочник, задачи и примеры Express отдаёт готовым HTML. Шаблонизатора нет: страницы собираются шаблонными строками JavaScript с функцией esc() на каждом вставляемом значении.

Один источник данных. Описания видов диаграмм, справка по конструкциям, задачи и примеры лежат в ES-модулях frontend/src/lib/*.js. Их импортирует и редактор в браузере, и сервер, а Dockerfile копирует папку lib в рабочий образ. Правка в одном месте меняет и сайт и подсказки в редакторе.

Картинки для статей рисует сам сервер. Адреса вида /api/examples/<вид>.svg и /api/tasks/<задача>.svg идут через PlantUML или рендерер ГОСТ. Ответ кешируется в памяти процесса и отдаётся с Cache-Control: max-age=86400. В robots.txt эти адреса разрешены, хотя остальной /api/ закрыт, иначе картинки не попадут в поиск. В каждой странице есть разметка schema.org в JSON-LD: TechArticle, BreadcrumbList, а на посадочных страницах ещё WebApplication и FAQPage.

Переходы без перезагрузки. При обычных ссылках боковая панель на 70 пунктов на каждом переходе сбрасывала прокрутку и сворачивала группы. Решение в духе PJAX заняло около 50 строк без фреймворка:

const doc = new DOMParser().parseFromString(await (await fetch(url)).text(), "text/html");
const art = doc.querySelector(".docs article");
if (!art) throw "no-docs";                 // чужая страница - обычный переход
history.pushState({ docs: 1 }, "", url);
document.querySelector(".docs article").replaceWith(document.adoptNode(art));
document.title = doc.title;               // + description, canonical и JSON-LD
ym(METRIKA_ID, "hit", url, { title: doc.title, referer: prev }); // счётчик видит переход

При быстрых щелчках предыдущий запрос отменяется через AbortController. Любая ошибка превращается в обычный переход по ссылке, так что худший случай это поведение [8] обычного сайта. В тесте переходы занимали 60-240 мс, включая “Назад” через popstate.

Нагрузочный тест и что он нашёл

Сервер самый дешёвый: 2 vCPU, 2 ГБ памяти, 525 ₽ в месяц. Тест шёл из браузера на домашнем интернете: пачки fetch по HTTP/2, смесь страниц справочника, редактора и SVG-картинок, с cache: "no-store" и уникальным параметром в URL, чтобы не попадать в кеш.

Тест

Результат

400 запросов одновременно

0 ошибок, медиана 433 мс, максимум 702 мс

25 секунд, 120 потоков без перерыва

13 287 запросов (~530 в секунду), 227 МБ, 0 ошибок; p50 191 мс, p95 426 мс, p99 666 мс

58 отрисовок PlantUML одновременно, в том числе диаграммы на 30–65 классов

все успешны за 1,8 с, SVG до 138 КБ

61-я отрисовка за минуту

429 “Слишком много отрисовок подряд”

40 сохранений диаграмм одновременно в SQLite

все 201 за 213 мс

Тело запроса 2 МБ, битый JSON, имя длиннее 120 символов

413, 400, 400 с понятным текстом

Имя диаграммы <img src=x>

сохраняется как текст, в интерфейсе не выполняется

За 25 секунд скорость упала примерно вдвое без роста ошибок. 227 МБ за 25 секунд это ~70 Мбит/с, так что похоже на потолок канала, а не процессора. Одного клиента для точного ответа мало. Ответы сжимает Caddy (encode zstd gzip), страница справочника весит 20-26 КБ до сжатия.

Баг 1: главная съедала лимит. Галерея на главной рисовала превью 16 видов диаграмм через тот же POST /api/render, что и редактор. Четыре возврата на главную за минуту съедали весь лимит в 60 отрисовок, и редактор внезапно переставал рисовать. Со стороны это выглядело как будто сайт тормозит. Теперь превью берутся GET-запросом /api/examples/<вид>.svg. Их кеширует браузер, а в лимит они не входят. Заодно картинка кладётся в клиентский кеш по коду шаблона, и открытый из галереи шаблон не рисуется заново. Было 16 POST на каждое открытие главной ,а стало 0.

Баг 2: “если” без условия. Строка если без условия не совпадала с регуляркой ^еслиs+(.+) и молча рисовалась прямоугольником с текстом “если”. Теперь такие строки это ошибка с подсказкой: “После “если” нужно условие, например, если x > 0″.

Как это было

Всё делалось в приложении Claude на ПК. Нейросеть не только писала код, но и сама складывала файлы в папку проекта, запускала тесты и проверяла готовый сайт в браузере.

  1. Идея. Я написал одним сообщением: нужен сайт как plantuml.com, с современным интерфейсом и разделением по видам диаграмм. Через несколько минут был первый рабочий вариант.

  2. Правки. Дальше шли обычные замечания, как дизайнеру, тёмная тема слишком мрачная, сделай в синих тонах. Я присылал скриншот и писал, что не так, а Claude исправлял. Не всегда с первого раза.

  3. Свой сервер. Я арендовал недорогой сервер в Москве, а нейросеть подготовила скрипт deploy.bat. Дважды щёлкаешь, вводишь пароль и скрипт по SSH копирует проект, ставит Docker и запускает docker compose up -d --build. С доменом я немного застрял: купил его, а сайт по адресу не открывался. Оказалось, забыл создать зону в DNS-хостинге.

  4. Рост. Потом появились блок-схемы по ГОСТ, задачи, редактор JSON и нагрузочный тест. Сейчас в проекте около 7 тысяч строк своего JavaScript и 50 тестов на node:test, из зависимостей Express, Mermaid и Vite.

За всё время я не написал ни строчки кода сам, только объяснял словами и скриншотами.

Что я вынес из этого

Нейросеть написала весь код и тесты, посоветовала сервер и сама находила свои ошибки. Я честно не знал, что такое NS-записи, она объяснила на скриншотах моей же панели.

Технический вывод: из 2 багов из раздела про тест юнит-тесты не поймали ни одного. ИИ хорошо пишет код и тесты к нему, но тесты повторяют [9] его же представления о том, как работает внешний мир. Ошибки нашлись, когда я попросил проверить настоящий сайт как тестировщик, и когда YAML сверялся с чужим парсером.

Оплата, пароли и настройки в личных кабинетах хостинга, Яндекса и Google остались на мне – это нейросеть за тебя не сделает.

Сейчас сделать свой небольшой сервис может человек, который не пишет код сам. Но понимать, что ты хочешь получить, и проверять результат всё равно придётся.

Что дальше

  • Виртуальный редактор кода вместо textarea – для файлов в несколько мегабайт.

  • Ссылка “поделиться”, чтобы отправить диаграмму коллеге или преподавателю.

  • Необязательный вход, чтобы диаграммы не пропадали при смене браузера.

Если какая-то диаграмма не рисуется, отправьте код в комментарии. Попробовать можно на umleditor.ru [1].

Автор: Viktor_Murz

Источник [10]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/36881

URLs in this post:

[1] umleditor.ru: https://umleditor.ru

[2] Редактор JSON: https://umleditor.ru/redaktor-json

[3] Справочник: https://umleditor.ru/uml

[4] 20 задач с блок-схемами: https://umleditor.ru/zadachi

[5] готовые примеры: https://umleditor.ru/primery

[6] ошибка: http://www.braintools.ru/article/4192

[7] памяти: http://www.braintools.ru/article/4140

[8] поведение: http://www.braintools.ru/article/9372

[9] повторяют: http://www.braintools.ru/article/4012

[10] Источник: https://habr.com/ru/articles/1093680/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1093680

www.BrainTools.ru

Rambler's Top100