| Server IP : 37.9.174.138 / Your IP : 216.73.216.117 Web Server : Apache System : Linux vps6.backend.sk 6.12.100+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.100-1 (2026-07-30) x86_64 User : ftpuser ( 1001) PHP Version : 8.4.24 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/html/backend-accounting.sk/app/modules/documentation/ |
Upload File : |
<?php
declare(strict_types=1);
/**
* UTF-8 HTML view of a module markdown doc (Bootstrap 5 docs layout).
*
* URL: /modules/documentation/view.php?md={module}/docs/{file}.md
* Example: /modules/documentation/view.php?md=worksheets/docs/MESACNA-KONTROLA.md
*
* Access: logged-in staff only (not customers).
*/
session_start();
$ROOT_FOLDER = 'modules';
$_SERVER['DOCUMENT_ROOT'] = substr(__DIR__, 0, strpos(__DIR__, $ROOT_FOLDER));
require $_SERVER['DOCUMENT_ROOT'] . 'config.php';
require $_SERVER['DOCUMENT_ROOT'] . 'app/classes/App.php';
require $_SERVER['DOCUMENT_ROOT'] . 'vendor/autoload.php';
App::run();
try {
$user = \app\classes\BasicUser::getLoggedUser();
if ($user->isCustomer()) {
throw new \Exception('Forbidden', \app\constants\HttpReponseCodes::HTTP_FORBIDDEN);
}
} catch (\Exception $ex) {
header('HTTP/1.0 403 Forbidden');
header('Content-Type: text/plain; charset=UTF-8');
echo 'Prístup zamietnutý.';
exit;
}
$mdRel = isset($_GET['md']) ? (string) $_GET['md'] : '';
$mdRel = str_replace('\\', '/', $mdRel);
$mdRel = ltrim($mdRel, '/');
if (!preg_match('#^[a-zA-Z0-9_-]+/docs/[a-zA-Z0-9_.-]+\.md$#', $mdRel)) {
header('Content-Type: text/plain; charset=UTF-8', true, 404);
echo 'Dokument sa nenašiel.';
exit;
}
$projectRoot = rtrim((string) $_SERVER['DOCUMENT_ROOT'], '/\\');
$modulesRoot = $projectRoot . DIRECTORY_SEPARATOR . 'modules';
$mdPath = $modulesRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $mdRel);
$mdReal = realpath($mdPath);
$modulesReal = realpath($modulesRoot);
if (
$mdReal === false
|| $modulesReal === false
|| strpos($mdReal, $modulesReal . DIRECTORY_SEPARATOR) !== 0
|| !is_readable($mdReal)
) {
header('Content-Type: text/plain; charset=UTF-8', true, 404);
echo 'Dokument sa nenašiel.';
exit;
}
header('Content-Type: text/html; charset=UTF-8');
/**
* @return array{
* introHtml: string,
* sections: list<array{id: string, title: string, bodyHtml: string}>,
* nav: list<array{id: string, text: string, level: int}>
* }
*/
function documentationDocsParseMarkdown(string $md): array
{
$lines = preg_split('/\r\n|\r|\n/', $md) ?: [];
$nav = [];
$usedIds = [];
$introParts = [];
$sections = [];
$currentSection = null;
$i = 0;
$n = count($lines);
$skipFirstH1 = true;
$append = static function (string $html) use (&$introParts, &$currentSection): void {
if ($currentSection === null) {
$introParts[] = $html;
return;
}
$currentSection['bodyParts'][] = $html;
};
$closeSection = static function () use (&$sections, &$currentSection): void {
if ($currentSection === null) {
return;
}
$sections[] = [
'id' => $currentSection['id'],
'title' => $currentSection['title'],
'bodyHtml' => implode("\n", $currentSection['bodyParts']),
];
$currentSection = null;
};
while ($i < $n) {
$line = $lines[$i];
if ($line === '---') {
$i++;
continue;
}
if (preg_match('/^(#{1,6})\s+(.+)$/', $line, $m)) {
$level = strlen($m[1]);
$level = min(max($level, 1), 6);
$plain = trim(strip_tags(preg_replace('/\*\*(.+?)\*\*/', '$1', $m[2]) ?? $m[2]));
if ($level === 1 && $skipFirstH1) {
$skipFirstH1 = false;
$i++;
continue;
}
$id = documentationDocsHeadingId($plain, $usedIds);
$headingHtml = documentationDocsInline($m[2]);
if ($level === 2) {
$closeSection();
$nav[] = [
'id' => $id,
'text' => $plain,
'level' => 2,
];
$currentSection = [
'id' => $id,
'title' => $headingHtml,
'bodyParts' => [],
];
$i++;
continue;
}
if ($level === 3 || $level === 4 || $level === 5 || $level === 6) {
if ($level === 3) {
$nav[] = [
'id' => $id,
'text' => $plain,
'level' => 3,
];
}
$anchor = '<a class="docs-heading-anchor text-decoration-none" href="#'
. htmlspecialchars($id, ENT_QUOTES, 'UTF-8')
. '" aria-label="Odkaz na sekciu">#</a>';
$append(
'<h' . $level . ' id="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '" class="docs-heading">'
. $headingHtml . ' ' . $anchor
. '</h' . $level . '>'
);
$i++;
continue;
}
$i++;
continue;
}
if (preg_match('/^\|\s*.+\s*\|$/', $line)) {
$tableLines = [];
while ($i < $n && preg_match('/^\|\s*.+\s*\|$/', $lines[$i])) {
$tableLines[] = $lines[$i];
$i++;
}
$append(documentationDocsTable($tableLines));
continue;
}
if (preg_match('/^-\s+(.+)$/', $line, $m)) {
$items = [];
while ($i < $n && preg_match('/^-\s+(.+)$/', $lines[$i], $lm)) {
$items[] = '<li>' . documentationDocsInline($lm[1]) . '</li>';
$i++;
}
$append('<ul class="mb-3">' . implode('', $items) . '</ul>');
continue;
}
if (preg_match('/^\d+\.\s+(.+)$/', $line, $m)) {
$items = [];
while ($i < $n && preg_match('/^\d+\.\s+(.+)$/', $lines[$i], $lm)) {
$items[] = '<li>' . documentationDocsInline($lm[1]) . '</li>';
$i++;
}
$append('<ol class="mb-3">' . implode('', $items) . '</ol>');
continue;
}
if (trim($line) === '') {
$i++;
continue;
}
$para = [];
while ($i < $n && trim($lines[$i]) !== '' && !preg_match('/^(#{1,6}\s|-\s|\d+\.\s|\||---$)/', $lines[$i])) {
$para[] = $lines[$i];
$i++;
}
$append('<p class="mb-3">' . documentationDocsInline(implode(' ', $para)) . '</p>');
}
$closeSection();
return [
'introHtml' => implode("\n", $introParts),
'sections' => $sections,
'nav' => $nav,
];
}
/**
* @param array<string, true> $usedIds
*/
function documentationDocsHeadingId(string $text, array &$usedIds): string
{
$map = [
'á' => 'a', 'ä' => 'a', 'č' => 'c', 'ď' => 'd', 'é' => 'e', 'í' => 'i',
'ĺ' => 'l', 'ľ' => 'l', 'ň' => 'n', 'ó' => 'o', 'ô' => 'o', 'ŕ' => 'r',
'š' => 's', 'ť' => 't', 'ú' => 'u', 'ý' => 'y', 'ž' => 'z',
'Á' => 'a', 'Ä' => 'a', 'Č' => 'c', 'Ď' => 'd', 'É' => 'e', 'Í' => 'i',
'Ĺ' => 'l', 'Ľ' => 'l', 'Ň' => 'n', 'Ó' => 'o', 'Ô' => 'o', 'Ŕ' => 'r',
'Š' => 's', 'Ť' => 't', 'Ú' => 'u', 'Ý' => 'y', 'Ž' => 'z',
];
$slug = strtr($text, $map);
$slug = strtolower($slug);
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? 'sekcia';
$slug = trim($slug, '-');
if ($slug === '') {
$slug = 'sekcia';
}
$base = $slug;
$n = 2;
while (isset($usedIds[$slug])) {
$slug = $base . '-' . $n;
$n++;
}
$usedIds[$slug] = true;
return $slug;
}
function documentationDocsInline(string $text): string
{
$text = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$text = preg_replace('/`([^`]+)`/', '<code class="docs-code">$1</code>', $text) ?? $text;
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text) ?? $text;
return $text;
}
function documentationDocsTable(array $tableLines): string
{
if ($tableLines === []) {
return '';
}
$rows = [];
foreach ($tableLines as $rowLine) {
$cells = array_map('trim', explode('|', trim($rowLine, '|')));
$isSeparator = true;
foreach ($cells as $cell) {
if ($cell !== '' && !preg_match('/^:?-+:?$/', $cell)) {
$isSeparator = false;
break;
}
}
if ($isSeparator) {
continue;
}
$rows[] = $cells;
}
if ($rows === []) {
return '';
}
$html = '<div class="table-responsive mb-3"><table class="table table-sm table-bordered table-striped align-middle docs-table mb-0">';
$html .= '<thead><tr>';
foreach ($rows[0] as $cell) {
$html .= '<th scope="col">' . documentationDocsInline($cell) . '</th>';
}
$html .= '</tr></thead><tbody>';
for ($r = 1, $rc = count($rows); $r < $rc; $r++) {
$html .= '<tr>';
foreach ($rows[$r] as $cell) {
$html .= '<td>' . documentationDocsInline($cell) . '</td>';
}
$html .= '</tr>';
}
return $html . '</tbody></table></div>';
}
$md = (string) file_get_contents($mdReal);
$title = 'Dokumentácia';
if (preg_match('/^#\s+(.+)$/m', $md, $m)) {
$title = trim($m[1]);
}
$parsed = documentationDocsParseMarkdown($md);
$introHtml = $parsed['introHtml'];
$sections = $parsed['sections'];
$navItems = $parsed['nav'];
?>
<!DOCTYPE html>
<html lang="sk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></title>
<link href="/lib/bootstrap5/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--docs-accent: #0d6efd;
--docs-accent-soft: #e7f1ff;
--docs-accent-mid: #cfe2ff;
--docs-accent-dark: #0a58ca;
--docs-text: #495057;
--docs-text-strong: #212529;
--docs-bg: #f3f4f6;
}
body {
background: var(--docs-bg);
color: var(--docs-text);
}
.docs-layout {
max-width: 1140px;
margin: 0 auto;
}
/* Sticky TOC: own vertical scroll when taller than viewport (e.g. docs iframe modal). */
.docs-sidebar {
position: sticky;
top: 1.25rem;
align-self: start;
max-height: calc(100vh - 2.5rem);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
.docs-toc-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
padding: 1rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.04);
}
.docs-toc-title {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--docs-accent);
}
.docs-toc .nav-link {
color: #495057;
font-size: 0.9rem;
padding: 0.35rem 0.75rem;
border-left: 2px solid transparent;
border-radius: 0;
white-space: normal;
word-break: break-word;
}
.docs-toc .nav-link.docs-toc-h2 {
font-weight: 700;
color: #212529;
}
.docs-toc .nav-link:hover,
.docs-toc .nav-link:focus {
color: var(--docs-accent);
background: rgba(13, 110, 253, 0.06);
}
.docs-toc .nav-link.active {
color: var(--docs-accent);
font-weight: 700;
border-left-color: var(--docs-accent);
background: rgba(13, 110, 253, 0.08);
}
.docs-toc .nav-link.docs-toc-h3 {
padding-left: 1.5rem;
font-size: 0.85rem;
color: #6c757d;
font-weight: 400;
}
.docs-toc .nav-link.docs-toc-h3.active {
font-weight: 600;
}
.docs-mobile-toc-bar {
display: flex;
justify-content: flex-end;
margin-bottom: 1rem;
}
@media (min-width: 992px) {
.docs-mobile-toc-bar {
display: none;
}
}
.docs-intro-card,
.docs-section-card {
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.04);
overflow: hidden;
background: #fff;
}
.docs-intro-card .card-body {
padding: 1.35rem 1.75rem 1.6rem;
}
.docs-intro {
color: var(--docs-text);
}
.docs-intro > :last-child,
.docs-section-card .card-body > :last-child {
margin-bottom: 0 !important;
}
.docs-intro strong {
color: var(--docs-text-strong);
}
.docs-section-card {
scroll-margin-top: 1.25rem;
}
.docs-section-card .card-header {
background: linear-gradient(90deg, var(--docs-accent-soft) 0%, #f8fbff 100%);
border-bottom: 1px solid var(--docs-accent-mid);
padding: 0.95rem 1.35rem;
}
.docs-section-card .card-header h2 {
font-size: 1.2rem;
font-weight: 700;
margin: 0;
color: var(--docs-accent-dark);
letter-spacing: -0.015em;
scroll-margin-top: 1.25rem;
}
.docs-section-card .card-body {
padding: 1.35rem 1.5rem 1.6rem;
background: #fff;
color: var(--docs-text);
line-height: 1.6;
}
.docs-section-card .card-body > :last-child {
margin-bottom: 0 !important;
}
.docs-section-card .card-body strong {
color: var(--docs-text-strong);
font-weight: 600;
}
.docs-section-card h3 {
font-size: 1.02rem;
font-weight: 700;
margin-top: 1.5rem;
margin-bottom: 0.7rem;
padding: 0.35rem 0 0.35rem 0.8rem;
border-top: none;
border-left: 3px solid var(--docs-accent);
color: #084298;
background: transparent;
scroll-margin-top: 1.25rem;
}
.docs-section-card h3:first-child {
margin-top: 0.15rem;
padding-top: 0.35rem;
}
.docs-section-card h4,
.docs-section-card h5,
.docs-section-card h6 {
color: #0a58ca;
font-weight: 600;
margin-top: 1.1rem;
margin-bottom: 0.55rem;
scroll-margin-top: 1.25rem;
}
/* Explicit sizes: Bootstrap defaults make h4 larger than our custom h3 (1.02rem). */
.docs-section-card h4 {
font-size: 0.98rem;
}
.docs-section-card h5 {
font-size: 0.94rem;
}
.docs-section-card h6 {
font-size: 0.9rem;
}
.docs-section-card ul,
.docs-section-card ol {
padding-left: 1.2rem;
}
.docs-section-card li {
margin-bottom: 0.35rem;
}
.docs-section-card li::marker {
color: var(--docs-accent);
}
.docs-heading-anchor {
opacity: 0;
color: var(--docs-accent);
font-weight: 400;
margin-left: 0.25rem;
}
.docs-heading:hover .docs-heading-anchor,
.docs-section-card .card-header:hover .docs-heading-anchor {
opacity: 1;
}
.docs-code {
background: var(--docs-accent-soft);
color: var(--docs-accent-dark);
padding: 0.12em 0.4em;
border-radius: 0.25rem;
font-size: 0.9em;
border: 1px solid var(--docs-accent-mid);
}
.docs-table {
font-size: 0.925rem;
--bs-table-bg: #fff;
--bs-table-striped-bg: #f9fafb;
--bs-table-border-color: #e5e7eb;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
color: var(--docs-text);
}
.docs-table.table-bordered > :not(caption) > * {
border-width: 0;
}
.docs-table.table-bordered > :not(caption) > * > * {
border: 1px solid var(--bs-table-border-color);
}
.docs-table.table-bordered > :first-child > tr:first-child > * {
border-top-width: 0;
}
.docs-table.table-bordered > :last-child > tr:last-child > * {
border-bottom-width: 0;
}
.docs-table.table-bordered > :not(caption) > * > *:first-child {
border-left-width: 0;
}
.docs-table.table-bordered > :not(caption) > * > *:last-child {
border-right-width: 0;
}
.docs-table thead th {
white-space: nowrap;
font-weight: 700;
color: var(--docs-text-strong) !important;
background: #f3f4f6 !important;
border-bottom-color: #e5e7eb !important;
box-shadow: none;
}
.docs-table thead th strong,
.docs-table thead th code,
.docs-table thead th a {
color: var(--docs-text-strong) !important;
}
.docs-table thead th code {
background: #fff;
border: 1px solid #e5e7eb;
padding: 0.05em 0.3em;
border-radius: 0.2rem;
font-weight: 600;
}
.docs-table tbody td {
vertical-align: top;
}
.docs-offcanvas-toc .nav-link {
border-left: 2px solid transparent;
}
.docs-offcanvas-toc .nav-link.active {
border-left-color: var(--docs-accent);
color: var(--docs-accent);
font-weight: 700;
}
.btn-outline-secondary {
--bs-btn-color: var(--docs-accent-dark);
--bs-btn-border-color: var(--docs-accent-mid);
--bs-btn-hover-bg: var(--docs-accent);
--bs-btn-hover-border-color: var(--docs-accent);
--bs-btn-hover-color: #fff;
--bs-btn-active-bg: var(--docs-accent-dark);
--bs-btn-active-border-color: var(--docs-accent-dark);
}
</style>
</head>
<body tabindex="0">
<div class="container-xxl docs-layout px-3 px-lg-4 py-4">
<div class="docs-mobile-toc-bar">
<button class="btn btn-outline-secondary btn-sm" type="button"
data-bs-toggle="offcanvas" data-bs-target="#docsOffcanvas"
aria-controls="docsOffcanvas">
Obsah
</button>
</div>
<div class="row g-4">
<aside class="col-lg-3 d-none d-lg-block">
<nav id="docsToc" class="docs-sidebar" aria-label="Obsah dokumentu">
<div class="docs-toc-card">
<div class="docs-toc-title mb-2">Obsah</div>
<nav class="nav flex-column docs-toc">
<?php foreach ($navItems as $item): ?>
<a class="nav-link<?php echo $item['level'] === 3 ? ' docs-toc-h3' : ' docs-toc-h2'; ?>"
href="#<?php echo htmlspecialchars($item['id'], ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($item['text'], ENT_QUOTES, 'UTF-8'); ?>
</a>
<?php endforeach; ?>
</nav>
</div>
</nav>
</aside>
<main class="col-lg-9">
<div class="d-flex flex-column gap-3">
<?php if ($introHtml !== ''): ?>
<div class="card docs-intro-card">
<div class="card-body">
<div class="docs-intro">
<?php echo $introHtml; ?>
</div>
</div>
</div>
<?php endif; ?>
<?php foreach ($sections as $section): ?>
<section class="card docs-section-card" id="<?php echo htmlspecialchars($section['id'], ENT_QUOTES, 'UTF-8'); ?>">
<div class="card-header">
<h2 class="docs-heading">
<?php echo $section['title']; ?>
<a class="docs-heading-anchor text-decoration-none"
href="#<?php echo htmlspecialchars($section['id'], ENT_QUOTES, 'UTF-8'); ?>"
aria-label="Odkaz na sekciu">#</a>
</h2>
</div>
<div class="card-body">
<?php echo $section['bodyHtml']; ?>
</div>
</section>
<?php endforeach; ?>
</div>
</main>
</div>
</div>
<div class="offcanvas offcanvas-start" tabindex="-1" id="docsOffcanvas" aria-labelledby="docsOffcanvasLabel">
<div class="offcanvas-header border-bottom">
<h2 class="offcanvas-title fs-5" id="docsOffcanvasLabel">Obsah</h2>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Zavrieť"></button>
</div>
<div class="offcanvas-body">
<nav class="nav flex-column docs-toc docs-offcanvas-toc">
<?php foreach ($navItems as $item): ?>
<a class="nav-link<?php echo $item['level'] === 3 ? ' docs-toc-h3' : ' docs-toc-h2'; ?>"
href="#<?php echo htmlspecialchars($item['id'], ENT_QUOTES, 'UTF-8'); ?>"
data-bs-dismiss="offcanvas">
<?php echo htmlspecialchars($item['text'], ENT_QUOTES, 'UTF-8'); ?>
</a>
<?php endforeach; ?>
</nav>
</div>
</div>
<script src="/lib/bootstrap5/js/bootstrap.bundle.min.js"></script>
<script>
(function () {
var links = Array.prototype.slice.call(document.querySelectorAll('.docs-toc .nav-link'));
if (!links.length) {
return;
}
var byId = {};
links.forEach(function (link) {
var href = link.getAttribute('href') || '';
if (href.charAt(0) !== '#') {
return;
}
var id = href.slice(1);
if (!id) {
return;
}
if (!byId[id]) {
byId[id] = {
id: id,
el: document.getElementById(id),
links: []
};
}
byId[id].links.push(link);
});
var sections = Object.keys(byId).map(function (id) {
return byId[id];
}).filter(function (item) {
return !!item.el;
}).sort(function (a, b) {
if (a.el === b.el) {
return 0;
}
return (a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1;
});
if (!sections.length) {
return;
}
var offset = 96;
var sidebar = document.getElementById('docsToc');
var lastActiveId = '';
function ensureSidebarLinkVisible(link) {
if (!sidebar || !link || !sidebar.contains(link)) {
return;
}
var sidebarRect = sidebar.getBoundingClientRect();
var linkRect = link.getBoundingClientRect();
var pad = 8;
if (linkRect.top < sidebarRect.top + pad) {
sidebar.scrollTop += linkRect.top - sidebarRect.top - pad;
} else if (linkRect.bottom > sidebarRect.bottom - pad) {
sidebar.scrollTop += linkRect.bottom - sidebarRect.bottom + pad;
}
}
function setActive() {
var current = sections[0];
for (var i = 0; i < sections.length; i++) {
if (sections[i].el.getBoundingClientRect().top <= offset) {
current = sections[i];
}
}
links.forEach(function (link) {
link.classList.remove('active');
});
current.links.forEach(function (link) {
link.classList.add('active');
});
if (current.id !== lastActiveId) {
lastActiveId = current.id;
var sidebarLink = current.links.filter(function (link) {
return sidebar && sidebar.contains(link);
})[0];
ensureSidebarLinkVisible(sidebarLink);
}
}
var ticking = false;
window.addEventListener('scroll', function () {
if (ticking) {
return;
}
ticking = true;
window.requestAnimationFrame(function () {
setActive();
ticking = false;
});
}, {passive: true});
window.addEventListener('resize', setActive);
setActive();
})();
</script>
</body>
</html>