'...', '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', '

$1

', $html); $html = preg_replace('/^## (.+)$/m', '

$1

', $html); $html = preg_replace('/^# (.+)$/m', '

$1

', $html); // Жирный и курсив $html = preg_replace('/\*\*(.+?)\*\*/', '$1', $html); $html = preg_replace('/\*(.+?)\*/', '$1', $html); // Списки $html = preg_replace('/^- (.+)$/m', '
  • $1
  • ', $html); $html = preg_replace('/(
  • .*<\/li>)/s', '', $html); // Таблицы — группируем строки с | в блоки $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[] = '
    '; $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[] = "<$tag>" . implode("<$tag>", $cells) . ""; } else { if ($in_table) { $table_lines[] = '
    '; $in_table = false; } $sep_skipped = false; $table_lines[] = $line; } } if ($in_table) { $table_lines[] = ''; } $html = implode("\n", $table_lines); // Ссылки $html = preg_replace('/\[(.+?)\]\((.+?)\)/', '$1', $html); // Абзацы $paragraphs = explode("\n\n", $html); $html = ''; foreach ($paragraphs as $p) { $p = trim($p); if ($p === '') continue; if (strpos($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 []; }