'...', '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><$tag>", $cells) . "$tag>
";
} 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 [];
}