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 `${escape(text)}${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 = `

Aktueller Katalog

Preisanzeige

Aktuelle Preise werden geladen.

`; 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 ``; }).join(''); return `

    ${group}

    `; }).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
    enelixDokumentation
    Wissen / ${escape(page.group)}${escape(manifest.branch)} · ${manifest.branch === 'develop' ? 'Testing' : manifest.branch === 'beta' ? 'Beta' : 'Stable'}
    ${isPrices ? html.replace(/(]*>)/, `${pricing}$1`) : html}

    ${escape(source)}

    ${escape(page.source)} · Fragen und Antworten

    Git-Abgleich: ${escape(manifest.checkedAt)}${manifest.drafts ? ' · neue Anleitungen als Entwurf' : ''}

    ${isPrices ? `` : ''}`; }