feat(cms): add maintained user guide with Súgó menu entry
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
- docs/felhasznaloi-utmutato.md: user guide for the website and the CMS
(login, editing, arrays, save/validation, publish, security)
- /guide endpoint renders the markdown auth-protected via a dependency-free
renderer (scripts/markdown-render.js) in the CMS dark theme
- new ❓ Súgó entry in the CMS bottom bar
- steering rule: the guide must be updated in the same commit as any CMS or
website feature change
Closes MITHOME-57
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// WHY: the Content Editor runs on system Node without node_modules, so the user
|
||||
// guide (docs/felhasznaloi-utmutato.md) is rendered by this small dependency-free
|
||||
// markdown renderer instead of an external library.
|
||||
// Supported subset: headings (#..####), bold, inline code, links, ul/ol lists,
|
||||
// fenced code blocks, horizontal rules, paragraphs. HTML is escaped first.
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function renderInline(text) {
|
||||
return escapeHtml(text)
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
}
|
||||
|
||||
function renderMarkdown(markdown) {
|
||||
const lines = String(markdown).split('\n');
|
||||
const out = [];
|
||||
let listTag = null; // 'ul' | 'ol'
|
||||
let inCode = false;
|
||||
|
||||
const closeList = () => {
|
||||
if (listTag) {
|
||||
out.push(`</${listTag}>`);
|
||||
listTag = null;
|
||||
}
|
||||
};
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.trimEnd();
|
||||
|
||||
if (line.trim().startsWith('```')) {
|
||||
closeList();
|
||||
out.push(inCode ? '</code></pre>' : '<pre><code>');
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
if (inCode) {
|
||||
out.push(escapeHtml(raw));
|
||||
continue;
|
||||
}
|
||||
if (!line.trim()) {
|
||||
closeList();
|
||||
continue;
|
||||
}
|
||||
|
||||
const heading = line.match(/^(#{1,4})\s+(.*)$/);
|
||||
if (heading) {
|
||||
closeList();
|
||||
const level = heading[1].length;
|
||||
out.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
if (/^(-{3,}|\*{3,})$/.test(line.trim())) {
|
||||
closeList();
|
||||
out.push('<hr>');
|
||||
continue;
|
||||
}
|
||||
const unordered = line.match(/^\s*[-*]\s+(.*)$/);
|
||||
if (unordered) {
|
||||
if (listTag !== 'ul') {
|
||||
closeList();
|
||||
out.push('<ul>');
|
||||
listTag = 'ul';
|
||||
}
|
||||
out.push(`<li>${renderInline(unordered[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
const ordered = line.match(/^\s*\d+\.\s+(.*)$/);
|
||||
if (ordered) {
|
||||
if (listTag !== 'ol') {
|
||||
closeList();
|
||||
out.push('<ol>');
|
||||
listTag = 'ol';
|
||||
}
|
||||
out.push(`<li>${renderInline(ordered[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
closeList();
|
||||
out.push(`<p>${renderInline(line)}</p>`);
|
||||
}
|
||||
|
||||
closeList();
|
||||
if (inCode) out.push('</code></pre>');
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
module.exports = { renderMarkdown, renderInline, escapeHtml };
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Tests for the CMS user guide:
|
||||
* 1. markdown renderer unit checks (headings, bold, code, lists, links, escaping)
|
||||
* 2. /guide endpoint integration — auth-protected, serves the rendered guide
|
||||
* 3. the main editor page contains the Súgó menu link
|
||||
*/
|
||||
const assert = require('assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const { renderMarkdown } = require('../scripts/markdown-render');
|
||||
|
||||
// ── 1. Markdown renderer ─────────────────────────────────────────────────────
|
||||
|
||||
const rendered = renderMarkdown([
|
||||
'# Cím',
|
||||
'',
|
||||
'Ez **félkövér** és `kód`, valamint [link](https://example.com).',
|
||||
'',
|
||||
'- első',
|
||||
'- második',
|
||||
'',
|
||||
'1. lépés',
|
||||
'2. lépés',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<script>alert(1)</script>',
|
||||
].join('\n'));
|
||||
|
||||
assert.match(rendered, /<h1>Cím<\/h1>/);
|
||||
assert.match(rendered, /<strong>félkövér<\/strong>/);
|
||||
assert.match(rendered, /<code>kód<\/code>/);
|
||||
assert.match(rendered, /<a href="https:\/\/example\.com"[^>]*>link<\/a>/);
|
||||
assert.match(rendered, /<ul>\s*<li>első<\/li>\s*<li>második<\/li>\s*<\/ul>/);
|
||||
assert.match(rendered, /<ol>\s*<li>lépés<\/li>\s*<li>lépés<\/li>\s*<\/ol>/);
|
||||
assert.match(rendered, /<hr>/);
|
||||
// Raw HTML must be escaped, never executable
|
||||
assert.doesNotMatch(rendered, /<script>alert/);
|
||||
assert.match(rendered, /<script>/);
|
||||
console.log('Markdown renderer unit tests: OK');
|
||||
|
||||
// ── 2. /guide endpoint + Súgó menu link (real server) ───────────────────────
|
||||
|
||||
const PORT = 4124;
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const AUDIT_FILE = path.join(os.tmpdir(), `content-editor-audit-guide-${process.pid}.jsonl`);
|
||||
|
||||
const child = spawn('node', ['content-editor.js'], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
CONTENT_EDITOR_PORT: String(PORT),
|
||||
CONTENT_EDITOR_AUDIT_FILE: AUDIT_FILE,
|
||||
CMS_USER: 'guide-test-user',
|
||||
CMS_PASS: 'guide-test-pass',
|
||||
CMS_DEPLOY_ENV: 'staging',
|
||||
},
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
async function waitForServer(timeoutMs = 10000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
await fetch(`${BASE}/logout`); // rate-limit-free readiness probe
|
||||
return;
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
throw new Error('server did not start');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await waitForServer();
|
||||
|
||||
const auth = 'Basic ' + Buffer.from('guide-test-user:guide-test-pass').toString('base64');
|
||||
|
||||
// /guide requires authentication
|
||||
const unauth = await fetch(`${BASE}/guide`);
|
||||
assert.equal(unauth.status, 401);
|
||||
|
||||
// /guide serves the rendered markdown as HTML
|
||||
const guide = await fetch(`${BASE}/guide`, { headers: { Authorization: auth } });
|
||||
assert.equal(guide.status, 200);
|
||||
assert.match(guide.headers.get('content-type') || '', /text\/html/);
|
||||
const guideHtml = await guide.text();
|
||||
assert.match(guideHtml, /Felhasználói útmutató/);
|
||||
assert.match(guideHtml, /<h2[^>]*>.*Content Editor/); // rendered from the markdown source
|
||||
|
||||
// The guide file must exist in the repo (maintenance contract)
|
||||
assert.ok(fs.existsSync(path.join(ROOT, 'docs', 'felhasznaloi-utmutato.md')));
|
||||
|
||||
// The editor page exposes the Súgó menu entry
|
||||
const editor = await fetch(`${BASE}/`, { headers: { Authorization: auth } });
|
||||
const editorHtml = await editor.text();
|
||||
assert.match(editorHtml, /href="\/guide"[^>]*>❓ Súgó/);
|
||||
|
||||
console.log('Content Editor guide endpoint test: OK');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(err => { console.error('❌', err.message); process.exitCode = 1; })
|
||||
.finally(() => {
|
||||
child.kill('SIGTERM');
|
||||
try { fs.unlinkSync(AUDIT_FILE); } catch { /* already gone */ }
|
||||
});
|
||||
Reference in New Issue
Block a user