Initial commit

This commit is contained in:
2026-05-17 05:22:06 +03:00
commit ca4d00c895
155 changed files with 45216 additions and 0 deletions
+379
View File
@@ -0,0 +1,379 @@
<?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 [];
}