import { posix } from 'node:path';
import { Marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
export const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
export const slug = value => value.toLowerCase().replace(/<[^>]*>/g, '').replace(/[^\p{L}\p{N}\s_-]/gu, '').trim().replace(/\s/g, '-');
export function resolveLink(href, page, pages) {
if (!href || /[\u0000-\u0020\\]/.test(href)) return null;
if (href.startsWith('#')) return href;
if (href === '/') return '/';
const [path, anchor] = href.split('#', 2);
let key;
if (/^https:\/\/git\.belevo\.ch\/ENELIX\/(Enelix-EMS|Enelix-Utils)\/src\/branch\/[^/]+\//.test(path)) {
const match = path.match(/^https:\/\/git\.belevo\.ch\/ENELIX\/(Enelix-EMS|Enelix-Utils)\/src\/branch\/[^/]+\/(.+)$/);
key = `${match[1]}/${match[2]}`;
} else if (/^https:\/\//.test(href)) {
try { const url = new URL(href); return url.username || url.password ? null : url.href; } catch { return null; }
} else if (/^[\w+.-]+:|^\/\//.test(href) || path.startsWith('/')) return null;
else key = posix.normalize(posix.join(page.repo, posix.dirname(page.source), path));
const target = pages.find(p => `${p.repo}/${p.source}` === key);
return target ? `/docs/${target.output}${anchor ? `#${anchor}` : ''}` : null;
}
export function resolveImage(href, page, images) {
if (!/^images\/[a-z0-9-]+\.png$/.test(href || '') || page.repo !== 'Enelix-EMS' || !/^docs\/public\/[a-z0-9-]+\.md$/.test(page.source)) return null;
return images.find(image => image.repo === page.repo && image.source === `docs/public/${href}`) || null;
}
export function renderMarkdown(page, pages, images = []) {
const toc = [];
const ids = new Map();
const marked = new Marked({ gfm: true, renderer: {
html() { return ''; },
image({ href, text, title }) {
const image = resolveImage(href, page, images);
if (!image) return `${escape(text)}`;
return `
${title ? `${escape(title)}` : ''}`;
},
heading({ tokens, depth }) {
const text = this.parser.parseInline(tokens);
const base = slug(sanitizeHtml(text, { allowedTags: [], allowedAttributes: {} }));
const count = ids.get(base) || 0;
ids.set(base, count + 1);
const id = `${base}${count ? `-${count}` : ''}`;
if (depth === 2) toc.push({ id, title: sanitizeHtml(text, { allowedTags: [], allowedAttributes: {} }) });
return `${text}#`;
},
link({ href, tokens }) {
const label = this.parser.parseInline(tokens);
const target = resolveLink(href, page, pages);
return target ? `${label}` : `${label}`;
},
table(token) { return `
${this.constructor.prototype.table.call(this, token)}
`; }
} });
const html = sanitizeHtml(marked.parse(page.markdown), {
allowedTags: [...sanitizeHtml.defaults.allowedTags, 'h1', 'h2', 'span', 'img'],
allowedAttributes: { a: ['href', 'class', 'aria-label'], '*': ['id'], span: ['class', 'title'], img:['src','alt','width','height','loading','decoding'], div: ['class', 'tabindex', 'role', 'aria-label'], th: ['align'], td: ['align'], code: ['class'] },
allowedSchemes: ['https'], allowProtocolRelative: false
});
return { html, toc };
}
const pricing = ``;
const referenceFolders = [
{ id:'verbraucher', title:'Verbraucher', outputs:['Batterie', 'Wassererwaermer', 'Pufferspeicher', 'Verbraucher-1-Stufig', 'Waermepumpe', 'Ladestation-Stand-Alone', 'Ladestation-Gateway', 'Easee-Gateway'].map(name => `ems/${name}.html`) },
{ id:'schnittstellen', title:'Schnittstellen', outputs:['ems/schnittstelle.html', 'ems/batterieschnittstelle.html', 'ems/easee-schnittstelle.html'] }
];
export function renderNavigation(current, pages) {
const link = (page, folder = '') => `${escape(page.title)}`;
return ['Anleitungen', 'Enelix EMS', 'Enelix Utils'].map(group => {
const members = pages.filter(page => page.group === group);
const rendered = new Set();
const entries = members.map(page => {
const folder = referenceFolders.find(item => item.outputs.includes(page.output));
if (!folder) return link(page);
if (rendered.has(folder.id)) return '';
rendered.add(folder.id);
const children = members.filter(child => folder.outputs.includes(child.output));
const active = children.some(child => child.output === current.output);
return `${folder.title}
${children.map(child => link(child, folder.title)).join('')}
`;
}).join('');
return ``;
}).join('');
}
export function renderPage(page, pages, manifest, assetVersion) {
const { html, toc } = renderMarkdown(page, pages, manifest.images || []);
const isPrices = page.output === 'preise.html';
const nav = renderNavigation(page, pages);
const source = page.commit ? `${page.repo} · ${page.commit.slice(0, 12)}` : 'Redaktioneller Entwurf · Git-Veröffentlichung ausstehend';
return `${escape(page.title)} | Enelix Dokumentation
Zum Inhalt
Wissen / ${escape(page.group)}${escape(manifest.branch)} · ${manifest.branch === 'develop' ? 'Testing' : manifest.branch === 'beta' ? 'Beta' : 'Stable'}
${isPrices ? html.replace(/(]*>)/, `${pricing}$1`) : html}
${isPrices ? `` : ''}`;
}