Files
site_aegisone/service/inc/docs_functions.php
T
2026-05-17 05:22:06 +03:00

380 lines
13 KiB
PHP
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.
<?php
/**
* AegisOne Engineering — Функции управления документацией в Markdown
*
* Обеспечивает:
* - Автоматическое обнаружение .md-файлов в service/docs/
* - Разграничение доступа по ролям (owner — всё, engineer — только разрешённое)
* - Хранение прав в JSON-файле service/permissions/docs_permissions.json
*/
define('DOCS_DIR', __DIR__ . '/../docs');
define('DOCS_PERMISSIONS_FILE', __DIR__ . '/../permissions/docs_permissions.json');
/**
* Сканирует папку docs/ и возвращает массив документов.
* Автоматически добавляет новые файлы в файл прав (если их там нет).
*
* @return array Массив документов: [['slug'=>'...', 'title'=>'...', 'filename'=>'...'], ...]
*/
function get_docs_list(): array
{
if (!is_dir(DOCS_DIR)) {
return [];
}
$files = glob(DOCS_DIR . '/*.md');
if ($files === false || empty($files)) {
return [];
}
$docs = [];
$permissions = load_permissions();
foreach ($files as $path) {
$filename = basename($path);
$slug = doc_slug($filename);
$title = doc_title($slug, $filename);
// Автоматически добавляем новый документ в права, если его ещё нет
if (!isset($permissions[$slug])) {
$permissions[$slug] = [
'title' => $title,
'filename' => $filename,
'sort_order' => count($permissions),
'engineer' => false,
];
}
// Миграция: старый boolean → новый array permissions
if (isset($permissions[$slug]['engineer']) && !isset($permissions[$slug]['permissions'])) {
$permissions[$slug]['permissions'] = [
'engineer' => [
'view' => !empty($permissions[$slug]['engineer']),
'edit' => false,
'cancel' => false,
]
];
}
// Если файл на диске отличается от сохранённого — обновляем
if ($permissions[$slug]['filename'] !== $filename) {
$permissions[$slug]['filename'] = $filename;
}
$docs[] = [
'slug' => $slug,
'title' => $permissions[$slug]['title'],
'filename' => $filename,
'sort_order' => $permissions[$slug]['sort_order'] ?? 0,
];
}
// Сохраняем, если были добавлены новые
save_permissions($permissions);
// Сортировка по sort_order (ручная), затем по title
usort($docs, function($a, $b) {
$soA = $a['sort_order'] ?? 0;
$soB = $b['sort_order'] ?? 0;
if ($soA !== $soB) return $soA - $soB;
return strcmp($a['title'], $b['title']);
});
return $docs;
}
/**
* Возвращает содержимое .md-файла по слагу.
* Ищет файл по slug, сканируя директорию (не полагается на сохранённое имя).
*
* @param string $slug Слаг документа
* @return string|null Содержимое файла или null, если не найден
*/
function get_doc_content(string $slug): ?string
{
$permissions = load_permissions();
if (!isset($permissions[$slug])) {
return null;
}
// Сначала пробуем сохранённое имя файла
$savedFilename = $permissions[$slug]['filename'];
$path = DOCS_DIR . '/' . $savedFilename;
if (file_exists($path)) {
$content = file_get_contents($path);
return $content !== false ? $content : null;
}
// Если не нашли — ищем по slug среди всех .md файлов
$files = glob(DOCS_DIR . '/*.md');
if ($files === false) return null;
foreach ($files as $filePath) {
$fn = basename($filePath);
$fSlug = doc_slug($fn);
if ($fSlug === $slug) {
// Обновляем сохранённое имя
$permissions[$slug]['filename'] = $fn;
save_permissions($permissions);
$content = file_get_contents($filePath);
return $content !== false ? $content : null;
}
}
return null;
}
/**
* Преобразует md-заголовки и ссылки в HTML.
* Простейший парсер для базовой читаемости.
*
* @param string $markdown Исходный Markdown
* @return string HTML
*/
function render_markdown_simple(string $markdown): string
{
$html = htmlspecialchars($markdown, ENT_NOQUOTES, 'UTF-8');
// Заголовки h1-h3
$html = preg_replace('/^### (.+)$/m', '<h3>$1</h3>', $html);
$html = preg_replace('/^## (.+)$/m', '<h2>$1</h2>', $html);
$html = preg_replace('/^# (.+)$/m', '<h1>$1</h1>', $html);
// Жирный и курсив
$html = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html);
$html = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $html);
// Списки
$html = preg_replace('/^- (.+)$/m', '<li>$1</li>', $html);
$html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html);
// Таблицы — группируем строки с | в блоки <table>
$lines = explode("\n", $html);
$in_table = false;
$sep_skipped = false;
$table_lines = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if (preg_match('/^\|.+\|$/', $trimmed)) {
if (!$in_table) {
$table_lines[] = '<table>';
$in_table = true;
$sep_skipped = false;
}
if (preg_match('/^\|[-:\s]+\|$/', $trimmed)) {
$sep_skipped = true;
continue;
}
$inner = substr($trimmed, 1, -1);
$cells = array_map('trim', explode('|', $inner));
$tag = $sep_skipped ? 'th' : 'td';
$sep_skipped = false;
$table_lines[] = "<tr><$tag>" . implode("</$tag><$tag>", $cells) . "</$tag></tr>";
} else {
if ($in_table) {
$table_lines[] = '</table>';
$in_table = false;
}
$sep_skipped = false;
$table_lines[] = $line;
}
}
if ($in_table) {
$table_lines[] = '</table>';
}
$html = implode("\n", $table_lines);
// Ссылки
$html = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2" target="_blank">$1</a>', $html);
// Абзацы
$paragraphs = explode("\n\n", $html);
$html = '';
foreach ($paragraphs as $p) {
$p = trim($p);
if ($p === '') continue;
if (strpos($p, '<h') === 0 || strpos($p, '<ul') === 0 || strpos($p, '<tr') === 0 || strpos($p, '<th') === 0 || strpos($p, '<table') === 0) {
$html .= $p . "\n";
} elseif (strpos($p, '<li') === 0) {
$html .= $p . "\n";
} else {
$html .= '<p>' . nl2br($p) . "</p>\n";
}
}
return $html;
}
/**
* Возвращает slug из имени файла (архитектура.md → arkhitektura).
*/
function doc_slug(string $filename): string
{
$slug = pathinfo($filename, PATHINFO_FILENAME);
// Транслитерация кириллицы в латиницу для URL
$converted = transliterate($slug);
return preg_replace('/[^a-z0-9_-]/', '', strtolower($converted));
}
/**
* Транслитерация кириллицы → латиница.
*/
function transliterate(string $text): string
{
$map = [
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'yo','ж'=>'zh',
'з'=>'z','и'=>'i','й'=>'y','к'=>'k','л'=>'l','м'=>'m','н'=>'n','о'=>'o',
'п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f','х'=>'kh','ц'=>'ts',
'ч'=>'ch','ш'=>'sh','щ'=>'shch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu',
'я'=>'ya','А'=>'A','Б'=>'B','В'=>'V','Г'=>'G','Д'=>'D','Е'=>'E','Ё'=>'Yo',
'Ж'=>'Zh','З'=>'Z','И'=>'I','Й'=>'Y','К'=>'K','Л'=>'L','М'=>'M','Н'=>'N',
'О'=>'O','П'=>'P','Р'=>'R','С'=>'S','Т'=>'T','У'=>'U','Ф'=>'F','Х'=>'Kh',
'Ц'=>'Ts','Ч'=>'Ch','Ш'=>'Sh','Щ'=>'Shch','Ъ'=>'','Ы'=>'Y','Ь'=>'','Э'=>'E',
'Ю'=>'Yu','Я'=>'Ya',' '=>'-','_'=>'-',
];
return strtr($text, $map);
}
/**
* Формирует человекочитаемый заголовок из слага.
* architecture → Архитектура
*/
function doc_title(string $slug, string $filename = ''): string
{
// обратная транслитерация для некоторых известных слагов
$known = [
'owner' => 'Owner',
'seo' => 'SEO',
'sla' => 'SLA',
'kpi-inzhenerov' => 'KPI инженеров',
'avtomaticheskaya-sistema-shs' => 'Автоматическая система SHS',
'oprosnik-raschet' => 'Опросник-расчёт',
'arkhitektura-kompanii' => 'Архитектура компании',
'kontent-plan' => 'Контент план',
'kp-i-struktura-audita' => 'КП и структура аудита',
'utp-i-kp' => 'УТП и КП',
'arkhetiktura-brenda' => 'Архетиктура бренда',
'marketing' => 'Маркеттинг',
'skripty-prodazh' => 'Скрипты продаж',
'osnova' => 'Основа',
'dokazatelnaya-ekspertiza' => 'Доказательная экспертиза',
'voronki-telegram-i-crm' => 'Воронки телеграм и CRM',
'voronka' => 'Воронка',
'sayt-i-upakovka' => 'Сайт и упаковка',
'dashboard-tekhnika' => 'Дашборд техника',
'dashboard-inzhenera' => 'Дашборд инженера',
'blog' => 'Блог',
'tsifrovaya-model-obshchaya' => 'Цифровая модель общая',
];
if (isset($known[$slug])) {
return $known[$slug];
}
// Если есть имя файла — используем его без .md
if ($filename !== '') {
return pathinfo($filename, PATHINFO_FILENAME);
}
return strtr($slug, ['-' => ' ', '_' => ' ']);
}
/**
* Загружает JSON-файл прав доступа.
*
* @return array
*/
function load_permissions(): array
{
if (!file_exists(DOCS_PERMISSIONS_FILE)) {
return [];
}
$json = file_get_contents(DOCS_PERMISSIONS_FILE);
$data = json_decode($json, true);
return is_array($data) ? $data : [];
}
/**
* Сохраняет JSON-файл прав доступа.
*
* @param array $permissions
* @return bool
*/
function save_permissions(array $permissions): bool
{
$dir = dirname(DOCS_PERMISSIONS_FILE);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
return file_put_contents(
DOCS_PERMISSIONS_FILE,
json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
) !== false;
}
/**
* Проверяет, разрешён ли документ для роли engineer.
*
* @param string $slug Слаг документа
* @return bool
*/
function is_doc_allowed_for_engineer(string $slug): bool
{
$permissions = load_permissions();
if (!isset($permissions[$slug])) return false;
$p = $permissions[$slug];
// Поддержка старого формата (boolean) и нового (array)
if (is_array($p) && isset($p['permissions'])) {
return !empty($p['permissions']['engineer']['view']);
}
return !empty($p['engineer']);
}
/**
* Проверяет конкретное право инженера для документа.
*
* @param string $slug Слаг документа
* @param string $action Действие: view, edit, cancel
* @return bool
*/
function is_engineer_allowed(string $slug, string $action): bool
{
$permissions = load_permissions();
if (!isset($permissions[$slug])) return false;
$p = $permissions[$slug];
if (is_array($p) && isset($p['permissions']['engineer'])) {
return !empty($p['permissions']['engineer'][$action]);
}
// Backward compat
if ($action === 'view') return !empty($p['engineer']);
return false;
}
/**
* Возвращает список документов, доступных для указанной роли.
*
* @param string $role Роль пользователя (owner|engineer|technician)
* @return array
*/
function get_allowed_docs(string $role): array
{
$all = get_docs_list();
if ($role === 'owner') {
return $all;
}
if ($role === 'engineer') {
return array_values(array_filter($all, function($d) {
return is_doc_allowed_for_engineer($d['slug']);
}));
}
// technician — нет доступа к документам
return [];
}