DevOps - это весело. А основа любого нестандартного DevOps - это соглашения. У нас в 1С есть одно более-менее часто встречающееся и, при этом, очень классное соглашение - это комментарии-описания методов. Сегодня мы посмотрим, как использовать данное соглашение в полную силу и извлечь из него максимум пользы - автоматизируем процесс создания документации
То, что будет здесь показано - лишь один из вариантов реализации, пусть и достаточно гибкий для изменения под свои нужды. Я буду опираться на формат комментариев из EDT, который несколько отличается от такового в Конфигураторе. Также вы сможете использовать другую, более близкую себе, систему формирования документов вместо той, которая будет здесь рассматриваться (если захотите) - данная реализация этому не препятствует
Зачем нужна автодокументация?
Наша документация, по итогу, будет представлять из себя набор документов в формате Markdown, которые мы пропустим через Docusaurus - генератор статических (только из файлов) веб-сайтов, который умеет создавать страницы на их основе. Но, раз уж мы делаем документацию из комментариев-описаний, то выходит, что вся информация в ней будет состоять лишь из того, что уже и так есть в модулях. Так зачем она нужна? Причин, на самом деле, довольно много:
- Системы для работы с документацией, которые и должны стать финальной точкой данного руководства, имеют такие классные вещи, как древовидная структура и глобальный поиск. Это очень сильно помогает в нахождении нужной информации - куда проще, чем шуршать по общим модулям (особенно, если речь о чужом коде)
- Большинство платформ для документации позволяют получать к ней доступ из любого места. В целом, большинство из них, как и Docusaurus, - это генераторы статический веб-сайтов, результат работы которых может быть развернут на любом хостинге
- Скрипт для разбора модулей - вещь гибкая. Здесь вы можете сами определить иерархию разделов, объединять документы по какому-либо принципу, добавлять признаки для последующего поиска по ним - все ограничивается лишь вашей фантазией (и возможностью поддерживать исполнение соглашений о форматах комментариев внутри вашей команды)
- В конце концов, вам не обязательно формировать всю документацию каждый раз с нуля. Достаточно простой проверки на существование файла с созданием нового только при его отсутствии - и документацию можно дополнять вручную. Это уменьшает гибкость и заставляет следить за актуальностью информации в документах при изменении модулей, но зато в сформированном документе вы сможете добавить любую дополнительную информацию: примечания, скриншоты, места использования, номера задач и пр.
Опыт организации подобного процесса я приобрел при разработке документации Открытого пакета интеграций - там вы можете посмотреть один из вариантов конечного результата. Суть работы заключается в следующем:
- Пишется скрипт на OneScript, который при запуске проходит модули конфигурации и вычленяет из них необходимую информацию
- Эта информация по заранее описанному шаблону записывается в MarkDown файлы
- Файлы помещаются в одну из возможных программ для работы с документацией: MkDocs, Docusaurus, Obsidian, миллионы их
Следовательно, нам понадобятся:
- OneScript с библиотекой osparser
- Docusaurus (я буду показывать на нем) или другая система для работы с MD
Начнем с написания скрипта на OneScript
Парсим модули
Подразумевается, что вы уже знакомы с написанием скриптов на OS в целом
Работать мы, само собой, будем с исходниками конфигурации, выгруженными в файлы. Бегло рассмотрим пример описания методов в 1С, с которым нам предстоит работать в дальнейшем:
// Скачать файл
// Скачивает файл с серверов Telegram
//
// Параметры:
// Токен - Строка - Токен
// IDФайла - Строка - ID файла для скачивания
//
// Возвращаемое значение:
// ДвоичныеДанные - данные файла
Функция СкачатьФайл(Знач Токен, Знач IDФайла) Экспорт
...
Это - стандартный комментарий, который формирует EDT. В данном случае он состоит из строки с названием и строки краткого описания в начале, далее идет стандартный блок Параметры, где каждый параметр состоит из трех "секций": имени, типа данных и описания. Завершается комментарий возвращаемым значением
Что мы можем принять здесь за постоянную? Это строение стандартных блоков Параметры и Возвращаемое значение, взяв за свое соглашение, разве что, запрет на использование знака дефис в описании - его мы будем использовать для разделения секций. Отдельным соглашением можно назвать использование двух первых строк как Названия и Описания метода - у вас это может быть какой-нибудь другой набор, но здесь я буду делить их так
Исходя из этого мы можем начать писать свой скрипт, но для начала нам надо а)найти необходимые файлы в исходниках б)пройти каждый файл по всем его функциям. С этого и начнем
#Использовать osparser
#Использовать fs
Перем ПутьКДокументации;
Процедура ПриСозданииОбъекта()
ФайлыМодулей = НайтиФайлы("./", "*.bsl", Истина); // Ищем все .bsl файлы
ПутьКДокументации = "./docs"; // Определяем каталог для сохранения документации
ФС.ОбеспечитьКаталог(ПутьКДокументации); // Создаем каталог, если его нет
Для Каждого Модуль Из ФайлыМодулей Цикл // Обходим каждый модуль
ИмяМодуля = Новый Файл(СтрЗаменить(Модуль.Путь, "Ext\", "")); // Получаем имя модуля
ИмяМодуля = ИмяМодуля.Имя;
ПутьМодуля = Модуль.ПолноеИмя;
// Создаем отдельный каталог под описание методов данного модуля
КаталогДокументацииМодуля = ФС.ОбъединитьПути(ПутьКДокументации, ИмяМодуля);
ФС.ОбеспечитьКаталог(КаталогДокументацииМодуля);
РазобратьМодуль(ПутьМодуля, КаталогДокументацииМодуля);
КонецЦикла;
КонецПроцедуры
Здесь я использую два сторонних пакета:
- osparser - для парсинга модулей (в коде выше еще не используется)
- fs - для упрощения работы с файлами и создания каталогов. Вы можете заменить его созданием каталогов стандартными методами, если вам так больше нравится
Что происходит в коде выше? Мы создаем пустой каталог для документации и ищем в исходниках все файлы .bsl - собственно, модули, в каталоге и подкаталогах. Потом для каждого модуля в основной папке документации создаем каталог с названием, равным его имени. В моем примере структура документации будет следующей:
- Модуль как основной раздел - каталог
- Область как подраздел - каталог
- Метод как документ - .md файл
- Область как подраздел - каталог
Пока ничего сложного. Теперь необходимо разложить сам текстовый документ модуля - за это будет отвечать процедура РазобратьМодуль, вызываемая в конце каждой итерации цикла:
Процедура РазобратьМодуль(ПутьМодуля, КаталогДокументации)
МассивОбластей = Новый Массив;
МассивМетодов = Новый Массив;
ДокументМодуля = Новый ТекстовыйДокумент();
ДокументМодуля.Прочитать(ПутьМодуля, "UTF-8"); // Читаем файл модуля
ТекстМодуля = ДокументМодуля.ПолучитьТекст(); // Получаем текст для парсера
Парсер = Новый ПарсерВстроенногоЯзыка; // Разбираем модуль
СтруктураМодуля = Парсер.Разобрать(ТекстМодуля);
ТекущиаяОбласть = "";
//Обходим все объявления методов, областей и пр.
Для Каждого Объявление Из СтруктураМодуля.Объявления Цикл
// Если это объявление метода - разбираем его комментарий и запиываем эта данные в структуру
Если Объявление.Тип = "ОбъявлениеМетода" Тогда
СтруктураДанныхМетода = РазобратьМетод(Объявление, ДокументМодуля);
Если ЗначениеЗаполнено(СтруктураДанныхМетода) Тогда
СтруктураДанныхМетода.Вставить("Область", ТекущиаяОбласть);
МассивМетодов.Добавить(СтруктураДанныхМетода);
КонецЕсли;
КонецЕсли;
// Если это область - записываем её, чтобы потом присвоить структуре данных методов
Если Объявление.Тип = "ИнструкцияПрепроцессораОбласть" Тогда
ТекущиаяОбласть = Объявление.Имя;
МассивОбластей.Добавить(ТекущиаяОбласть);
КонецЕсли;
// Возвращаемся к родительской области, если текущая область закончиалсь
Если Объявление.Тип = "ИнструкцияПрепроцессораКонецОбласти" Тогда
МассивОбластей.Удалить(МассивОбластей.ВГраница());
Если Не МассивОбластей.Количество() = 0 Тогда
ТекущиаяОбласть = МассивОбластей[МассивОбластей.ВГраница()];
Иначе
ТекущиаяОбласть = "";
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Формируем документы на основе разобранных модулей
СформироватьФайлыДокументации(МассивМетодов, КаталогДокументации);
КонецПроцедуры
В данной процедуре наша задача сформировать массив однотипных структур по всем методам модуля. Само формирование этих данных вынесено в следующую процедуру. Здесь же мы только ищем объявления методов для описания, а также объявления областей для группировки методов в документации.
Отдельно стоит остановится на парсере встроенного языка. Этот объект из пакета osparser разбирает переданный в него текст модуля на составные части, присущие языку: объявления, переменные, аннотации и пр. Примерно вот так будет выглядеть получаемая структура:
С этим мы и работаем: в частности нас интересуют номера строк объявлений методов. Перейдем к функции РазобратьМетод
Функция РазобратьМетод(Метод, ТекстовыйДокумент)
СтруктураДанных = Новый Структура();
НомерСтроки = Метод.Начало.НомерСтроки;
ИмяМетода = Метод.Сигнатура.Имя;
Объявление = "";
// Получаем строку объявления метода
Для Н = НомерСтроки По Метод.Конец.НомерСтроки Цикл
Часть = СокрЛП(ТекстовыйДокумент.ПолучитьСтроку(Н));
Объявление = Объявление + Часть;
Если Не ЗначениеЗаполнено(Часть) Тогда
Прервать;
КонецЕсли;
КонецЦикла;
МассивКомментария = РазобратьКомментарий(ТекстовыйДокумент, НомерСтроки);
МассивПараметров = Новый Массив;
МассивОписанийПараметров = Новый Массив;
Если МассивКомментария.Количество() = 0 Тогда
Возврат СтруктураДанных;
КонецЕсли;
СформироватьСтруктуруКомментария(МассивКомментария, МассивПараметров, СтруктураДанных);
СформироватьМассивОписанийПараметров(МассивПараметров, Метод, МассивОписанийПараметров);
СтруктураДанных.Вставить("ИмяМетода" , ИмяМетода);
СтруктураДанных.Вставить("Объявление", Объявление);
СтруктураДанных.Вставить("Параметры" , МассивОписанийПараметров);
Возврат СтруктураДанных;
КонецФункции
В начале мы сразу определяем номер строки в модуле, на которой начинается объявление текущего метода. Мы эту строку записываем для показа в документации, а номер запоминаем
Далее идет последовательная работа трех методов: РазобратьКомментарий, СформироватьСтруктуруКомментария и СформироватьМассивОписанийПараметров. Финальный результат формируется только в последней, а первые две используются для подготовки данных. Разберем их по порядку, начиная с РазобратьКомментарий:
Функция РазобратьКомментарий(Знач ТекстовыйДокумент, Знач НомерСтроки)
ТекущаяСтрока = ТекстовыйДокумент.ПолучитьСтроку(НомерСтроки - 1);
ТекстКомментария = ТекущаяСтрока;
Счетчик = 1;
Пока СтрНайти(ТекущаяСтрока, "//") > 0 Цикл //Начинаем проходить документ от объявления метода вверх
Счетчик = Счетчик + 1;
ТекущаяСтрока = ТекстовыйДокумент.ПолучитьСтроку(НомерСтроки - Счетчик);
ТекстКомментария = ТекущаяСтрока + Символы.ПС + ТекстКомментария;
КонецЦикла;
Если Счетчик = 1 Тогда
Возврат Новый Массив; // Пропуск, если нет комментария
КонецЕсли;
МассивКомментария = СтрРазделить(ТекстКомментария, "//", Ложь); // Делим комментарий по строкам
Если МассивКомментария.Количество() = 0 Тогда
Возврат Новый Массив;
Иначе
МассивКомментария.Удалить(0); // Удаляем объявление метода
КонецЕсли;
Возврат МассивКомментария;
КонецФункции
В этой функции мы берем за первую обрабатываемую строку объявление метода, после чего начинаем идти назад (вверх) по документу, записывая комментарий-описание. Далее этот комментарий просто разбивается в массив по символам "//"
Следующая процедура делит полученный массив комментария в более удобную для работы структуру: основные данные, вроде описания или возвращаемого значения, записываются уже непосредственно в финальную структуру описания (которая будет использована для формирования документа), а параметры пока просто добавляются построчно в массив (без деления на секции)
Процедура СформироватьСтруктуруКомментария(Знач МассивКомментария, МассивПараметров, СтруктураДанных)
ОписаниеМетода = "";
ЗаписыватьПараметры = Ложь;
ЗаписыватьОписание = Истина;
Счетчик = 0;
Для Каждого СтрокаКомментария Из МассивКомментария Цикл
Счетчик = Счетчик + 1;
Если Не ЗначениеЗаполнено(СокрЛП(СтрокаКомментария)) Тогда
// Завершение записи описания, если найдена пустая строка
ЗаписыватьОписание = Ложь;
КонецЕсли;
Если ЗаписыватьОписание = Истина И Счетчик > 1 Тогда
// Записываем описание из начала комментария
ОписаниеМетода = СокрЛП(ОписаниеМетода) + " " + СокрЛП(СтрокаКомментария);
КонецЕсли;
Если СтрНайти(СтрокаКомментария, "Параметры:") > 0 Тогда
// Начинаем записывать параметры, если найден признак
ЗаписыватьПараметры = Истина;
ЗаписыватьОписание = Ложь;
ИначеЕсли СтрНайти(СтрокаКомментария, "Возвращаемое значение:") > 0 Тогда
// Записываем возвращаемое значение, если найден признак
СтруктураДанных.Вставить("ВозвращаемоеЗначение", МассивКомментария[Счетчик]);
Прервать;
ИначеЕсли ЗаписыватьПараметры = Истина
И ЗначениеЗаполнено(СокрЛП(СтрокаКомментария))
И Не СтрНачинаетсяС(СокрЛП(СтрокаКомментария), "*") = 0 Тогда
// Записываем параметр, если он есть и это - не описание элемента коллекции
МассивПараметров.Добавить(СтрокаКомментария);
Иначе
Продолжить;
КонецЕсли;
КонецЦикла;
СтруктураДанных.Вставить("Описание" , ОписаниеМетода);
СтруктураДанных.Вставить("Заголовок", МассивКомментария[0]);
КонецПроцедуры
В последнем методе мы отдельно обрабатываем описания параметров из комментария:
Процедура СформироватьМассивОписанийПараметров(Знач МассивПараметров, Знач Метод, МассивОписанийПараметров)
Разделитель = "-";
Для Каждого ПараметрМетода Из МассивПараметров Цикл
МассивЭлементовПараметра = СтрРазделить(ПараметрМетода, Разделитель, Ложь);
КоличествоЭлементов = МассивЭлементовПараметра.Количество();
Для Н = 0 По МассивЭлементовПараметра.ВГраница() Цикл
МассивЭлементовПараметра[Н] = СокрЛП(МассивЭлементовПараметра[Н]);
КонецЦикла;
Имя1С = МассивЭлементовПараметра[0];
Типы = МассивЭлементовПараметра[1];
Описание = МассивЭлементовПараметра[2];
СтруктураПараметра = Новый Структура;
СтруктураПараметра.Вставить("Имя" , Имя1С);
СтруктураПараметра.Вставить("Типы" , Типы);
СтруктураПараметра.Вставить("Описание" , Описание);
СтруктураПараметра.Вставить("ЗначениеПоУмолчанию", ПолучитьЗначениеПараметраПоУмолчанию(Имя1С, Метод));
МассивОписанийПараметров.Добавить(СтруктураПараметра);
КонецЦикла;
КонецПроцедуры
Еще тут есть небольшая функция ПолучитьЗначениеПараметраПоУмолчанию: она лезет в объявление метода и забирает оттуда значение по умолчанию для необязательных параметров:
Функция ПолучитьЗначениеПараметраПоУмолчанию(Знач Имя, Знач Метод)
Значение = "";
Для Каждого ПараметрМетода Из Метод.Сигнатура.Параметры Цикл
Если ПараметрМетода.Имя = Имя Тогда
ЗначениеПараметра = ПараметрМетода.Значение;
Если ЗначениеЗаполнено(ЗначениеПараметра) Тогда
Попытка
Значение = ЗначениеПараметра["Элементы"][0]["Значение"];
Исключение
Значение = ЗначениеПараметра.Значение;
КонецПопытки;
Значение = ?(ЗначениеЗаполнено(Значение), Значение, "Пустое значение");
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат Значение;
КонецФункции
Теперь мы готовы формировать документацию. К этому моменту у нас есть по вот такой коллекции для каждого отдельного модуля конфигурации:
Осталось лишь сделать шаблон и записать туда эти данные
Шаблоны
Шаблону нужен нам для формирования конечных документов. Тут уже все зависит от вашей фантазии и того набора информации, которую вы будете получать из модулей вашей конфигурации. У меня этот шаблон выглядит так:
# @Заголовок
@Описание
*@Объявление*
| Параметр | CLI опция | Тип | Назначение |
|-|-|-|-|
@ТаблицаПараметров
Возвращаемое значение: @ВозвращаемоеЗначение
Шаблон довольно простой: есть заголовок, описание, возвращаемое значение, а также таблица с описанием параметров. Заполним документацию по этому шаблону, вернувшись в наш скрипт к процедуре СформироватьФайлыДокументации:
Процедура СформироватьФайлыДокументации(Методы, КаталогДокументации)
ДокументМакета = Новый ТекстовыйДокумент();
ДокументМакета.Прочитать("./template.md", "UTF-8");
// Обходим каждый метод модуля
Для Каждого СтруктураМетода Из Методы Цикл
КаталогОбласти = ФС.ОбъединитьПути(КаталогДокументации, СтруктураМетода["Область"]);
ФС.ОбеспечитьКаталог(КаталогОбласти);
Макет = ДокументМакета.ПолучитьТекст();
Макет = СтрЗаменить(Макет, "@Заголовок" , СтруктураМетода["Заголовок"]);
Макет = СтрЗаменить(Макет, "@Описание" , СтруктураМетода["Описание"]);
Макет = СтрЗаменить(Макет, "@Объявление" , СтруктураМетода["Объявление"]);
Макет = СтрЗаменить(Макет, "@ВозвращаемоеЗначение", СтруктураМетода["ВозвращаемоеЗначение"]);
ТаблицаПараметров = "";
// Формируем таблицу описания параметров
Для каждого ПараметрМетода Из СтруктураМетода["Параметры"] Цикл
ТаблицаПараметров = ТаблицаПараметров + " | "
+ ПараметрМетода.Имя + " | "
+ ПараметрМетода.Типы + " | "
+ ПараметрМетода.Описание + " |"
+ Символы.ПС;
КонецЦикла;
Макет = СтрЗаменить(Макет, "@ТаблицаПараметров", ТаблицаПараметров);
НовыйДокумент = Новый ТекстовыйДокумент();
НовыйДокумент.УстановитьТекст(Макет);
НовыйДокумент.Записать(КаталогОбласти + "/" + СтруктураМетода["ИмяМетода"] + ".md");
КонецЦикла;
КонецПроцедуры
В результате работы данной процедуры мы получаем следующую картину:
- В каталоге документации есть подкаталог для каждого модуля
- В каталогах модулей есть каталог для каждой области, если в ней есть методы
- В каталогах областей лежат документы для каждого метода
Данные документы уже можно просмотреть в любом визуализаторе для Markdown разметки
Но мы пойдем дальше и закинем эту документацию в новый инструмент...
Docusaurus
Docusaurus - это генератор статических веб-сайтов, который формирует страницы на основе переданного набора MD файлов. Для того, чтобы начать работу с ним, нам потребуется NPM - устанавливается он вместе с node.js, получить который можно тут: Ссылка
После установки NPM достаточно выбрать пустой каталог и в командной строке вызвать:
npx create-docusaurus@latest my-cool-1c-docs classic
Вместо my-cool-1c-docs можно выбрать свое имя проекта. Пути с кириллицей лучше не использовать, особенно если там есть пробелы. В процессе установки Docusaurus спросит, какой язык мы хотим использовать: JavaScript или TypeScript. Для нас это роли не играет - выбираем JavaScript
По окончании установки, каталог созданного проекта будет выглядеть так:
В цели данной статьи не входит описание тонкой настройки данного инструмента, поэтому на сегодня ограничимся просто выводом документации. Для этого нам достаточно очистить папку docs - удалять можно все, кроме документа intro.md - и закинуть туда свои MD файлы, сформированные ранее
Теперь проект можно запустить. Для этого достаточно вызвать следующую команду из папки проекта:
npx docusaurus start
Для сборки же готового сайта используется команда:
npx docusaurus build
Start используется для отладки, а build создает одноименную папку в каталоге проекта, которую в дальнейшем можно просто целиком зашвырнуть на любой хостинг, чтобы ваша документация была доступна из любого места
Получившаяся документация выглядит так:
Она не без проблем: названия областей и модулей выводятся в PascalCase, не настроено имя сайта и пр. Но это уже рабочий вариант, который можно кастомизировать далее как душе угодно: имена чинятся синонимайзером, а про файл настроек мы еще поговорим далее.
Вот пример документации ОПИ, все на этом же движке:
Поиск
Единственное, что я обещал, но не показал - поиск. Поиск добавляется как отдельный плагин в виде строки в правом верхнем углу. Ищет он по всей документации сразу, как в заголовках, так и в тексте документации
Чтобы этот поиск добавить, необходимо найти в каталоге проекта файл docusaurus.config.js и между блоками presers и themeConfig вставить следующий код:
plugins: [
[ require.resolve('docusaurus-lunr-search'), {
languages: ['en', 'ru'] // language codes
}]],
После чего просто вызвать в каталоге проекта команду
npm i docusaurus-lunr-search --save
Поиск будет отображаться при при любом запуске проекта, но работать - только в собранной версии (команда build). Протестировать собранную версию локально также можно. Для этого надо после build дополнительно вызвать
npx docusaurus serve
Отдельно нужно отметить сам файл docusaurus.config.js, в который мы вносили изменения. Это главный файл настроек Docusaurus и основной массив необходимых параметров находится именно там: имя сайта, лого, блоки, адрес и пр. Он достаточно обильно забит комментариями (на английском), так что с базовой настройкой не должно возникнуть проблем
В заключении
В первую очередь, данный механизм подойдет создателям собственных отдельных решений, однако даже внутри больших типовых конфигураций всегда найдутся отдельные места, подвластные такому способу документирования.
К главным плюсам можно отнести отсутствие необходимости со стороны разработчика отвлекаться на сторонние инструменты во время написания кода: основа будет создана автоматически, достаточно лишь попутного комментария. При этом, уже потом, после создания базовых документов, страницу любого отдельного метода можно дополнить информацией по своему усмотрению (не говоря уже о том, что и в сам комментарий можно напихать что угодно)
Спасибо за внимание!
Мой GitHub: https://gitub.com/Bayselonarrend Лицензия MIT: https://mit-license.org