UNPKG

@simplepg/repo

Version:

SimplePage repository

316 lines (283 loc) 11.2 kB
import { mimeType } from '@simplepg/common' export function populateTemplate(templateHtml, body, targetDomain, path, { title, description, 'sidebar-toc': sidebarToc } = {}, avatarPath = null, domainSuffix = '.link', faviconPaths = null) { const parser = new DOMParser() const templateDoc = parser.parseFromString(templateHtml, 'text/html') const rootElem = templateDoc.getElementById('content-container') rootElem.innerHTML = body const baseDomain = `${targetDomain}${domainSuffix}` const baseUrl = `https://${baseDomain}` const populateUrl = (path) => [baseUrl, ...(path.split('/').filter(Boolean))].join('/') const head = templateDoc.querySelector('head') const select = (name, isProp = false) => templateDoc.querySelector(`meta[${isProp ? 'property' : 'name'}="${name}"]`) const setMeta = (name, content, isProp = false) => { let meta = select(name, isProp) if (!meta) { meta = templateDoc.createElement('meta') meta.setAttribute(isProp ? 'property' : 'name', name) head.appendChild(meta) } meta.setAttribute('content', content) } const titleText = title || targetDomain const descriptionText = description || `A SimplePage by ${targetDomain}` const url = populateUrl(path) // HTML meta const titleElement = templateDoc.querySelector('title') titleElement.textContent = titleText setMeta('description', descriptionText) setMeta('ens-domain', targetDomain) setMeta('sidebar-toc', sidebarToc ? 'true' : 'false') // set favicon(s) const faviconLinks = Array.from(templateDoc.querySelectorAll('link[rel="icon"]')) const addFavicon = (href, media = null) => { const link = templateDoc.createElement('link') link.setAttribute('rel', 'icon') link.setAttribute('href', href) if (media) link.setAttribute('media', media) const type = mimeType(href) if (type) link.setAttribute('type', type) head.appendChild(link) return link } const clearFavicons = () => { faviconLinks.forEach(link => link.parentNode?.removeChild(link)) } let imageUrl = null const avatarImageUrl = avatarPath ? populateUrl(avatarPath) : null if (faviconPaths?.light || faviconPaths?.dark) { clearFavicons() const fallbackIcon = faviconPaths.light || faviconPaths.dark if (fallbackIcon) { addFavicon(fallbackIcon) } if (faviconPaths.light) { addFavicon(faviconPaths.light, '(prefers-color-scheme: light)') } if (faviconPaths.dark) { addFavicon(faviconPaths.dark, '(prefers-color-scheme: dark)') } imageUrl = populateUrl(fallbackIcon) } else if (avatarPath) { clearFavicons() addFavicon(avatarPath) imageUrl = populateUrl(avatarPath) } else if (faviconLinks.length > 0) { const href = faviconLinks[0].getAttribute('href') if (href) { imageUrl = populateUrl(href) } } else { clearFavicons() addFavicon('/_assets/images/favicon-light.png') addFavicon('/_assets/images/favicon-light.png', '(prefers-color-scheme: light)') addFavicon('/_assets/images/favicon-dark.png', '(prefers-color-scheme: dark)') imageUrl = populateUrl('/_assets/images/logo.png') } if (avatarImageUrl) { imageUrl = avatarImageUrl } else { imageUrl = populateUrl('/_assets/images/logo.png') } // set apple touch icon (iOS homescreen) const appleTouchIconHref = avatarPath && mimeType(avatarPath) !== 'image/svg+xml' ? avatarPath : '/_assets/images/logo.png' let appleTouchIcon = templateDoc.querySelector('link[rel="apple-touch-icon"]') if (!appleTouchIcon) { appleTouchIcon = templateDoc.createElement('link') appleTouchIcon.setAttribute('rel', 'apple-touch-icon') head.appendChild(appleTouchIcon) } appleTouchIcon.setAttribute('href', appleTouchIconHref) let twitterCardType = 'summary' // Check if body has any img tags and use the first one for social media const firstImgSrc = rootElem.querySelector('img')?.getAttribute('src') if (firstImgSrc) { imageUrl = populateUrl(firstImgSrc) twitterCardType = 'summary_large_image' } // Open Graph setMeta('og:url', url, true) setMeta('og:title', titleText, true) setMeta('og:description', descriptionText, true) if (imageUrl) setMeta('og:image', imageUrl, true) setMeta('og:site_name', targetDomain, true) // Twitter setMeta('twitter:card', twitterCardType) setMeta('twitter:domain', baseDomain) setMeta('twitter:url', url) setMeta('twitter:title', titleText) setMeta('twitter:description', descriptionText) if (imageUrl) setMeta('twitter:image', imageUrl) // RSS autodiscovery const rssLink = templateDoc.createElement('link') rssLink.setAttribute('rel', 'alternate') rssLink.setAttribute('type', 'application/rss+xml') rssLink.setAttribute('title', `${titleText} RSS Feed`) rssLink.setAttribute('href', '/rss.xml') head.appendChild(rssLink) return `<!DOCTYPE html>\n${templateDoc.documentElement.outerHTML}`; } export function populateManifest(domain, { title, description } = {}, avatarPath = null, avatarIcon = null) { const manifest = { name: title || domain, short_name: domain, description: description || `A SimplePage by ${domain}`, dapp_repository: "https://github.com/stigmergic-org/simplepage", dapp_contracts: [], icons: [], } const buildIcon = (src, type, sizes = null) => { const icon = { src, type } if (sizes) icon.sizes = sizes return icon } // If avatar is present, add it as an icon with proper MIME type if (avatarPath) { const avatarMimeType = mimeType(avatarPath) || 'image/svg+xml' const avatarSizes = avatarIcon?.sizes || (avatarMimeType === 'image/svg+xml' ? 'any' : null) manifest.icons.push(buildIcon(avatarPath, avatarMimeType, avatarSizes)) } else { manifest.icons.push(buildIcon("/_assets/images/logo.png", "image/png", "256x256")) } return JSON.stringify(manifest) } /** ----------------------------------------------- * DaisyUI Theme Integration for theme.css * ---------------------------------------------- */ // Import DaisyUI theme data import daisyuiThemes from 'daisyui/theme/object.js'; /** * Converts a theme object to CSS variables block * @param {Object} themeObj - Theme object with CSS variables * @returns {string} CSS variables block */ function toVarsBlock(themeObj) { return Object.entries(themeObj) .filter(([key]) => key.startsWith('--')) .map(([key, value]) => ` ${key}: ${value};`) .join('\n'); } /** * Generates theme CSS based on user preferences * @param {Object} themePrefs - User theme preferences * @param {string} themePrefs.light - Light theme name (default: 'light') * @param {string} themePrefs.dark - Dark theme name (default: 'dark') * @returns {string} Generated theme CSS */ export function populateTheme({ light = 'light', dark = 'dark' } = {}) { // Get theme data from DaisyUI const lightThemeData = daisyuiThemes[light] || daisyuiThemes.light; const darkThemeData = daisyuiThemes[dark] || daisyuiThemes.dark; // Generate CSS return `/* Generated theme.css - Light: ${light}, Dark: ${dark} */ :root:not([data-theme]) { ${toVarsBlock(lightThemeData)} } @media (prefers-color-scheme: dark) { :root:not([data-theme]) { ${toVarsBlock(darkThemeData)} } } `; } /** * Extracts frontmatter from markdown content. * Parses title, description, sidebar, RSS fields, and timestamps. * @param {string} markdown - The markdown content. * @returns {object} Object containing frontmatter data. */ export function parseFrontmatter(markdown) { const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*(?:\n([\s\S]*))?$/; const match = markdown.match(frontmatterRegex); if (!match) { return {} } const frontmatterLines = match[1].split('\n'); const frontmatter = {}; let isInTagsList = false; for (const line of frontmatterLines) { // Skip empty lines if (!line.trim()) continue; // Handle YAML list items (lines starting with -) if (line.trim().startsWith('-')) { if (isInTagsList) { const tagMatch = line.trim().match(/^-\s*(.+)$/); if (tagMatch) { let tagValue = tagMatch[1].trim(); // Remove quotes if present if ((tagValue.startsWith('"') && tagValue.endsWith('"')) || (tagValue.startsWith("'") && tagValue.endsWith("'"))) { tagValue = tagValue.slice(1, -1); } frontmatter.tags.push(tagValue); } } continue; } // Match key: value pattern const keyValueMatch = line.match(/^([^:]+):\s*(.*)$/); if (keyValueMatch) { const key = keyValueMatch[1].trim(); let value = keyValueMatch[2].trim(); // Reset tags list flag when we encounter a new key isInTagsList = false; // Parse text fields (title, description, language) if (key === 'title' || key === 'description' || key === 'language') { // Remove quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } frontmatter[key] = value; } // Parse boolean fields else if (key === 'sidebar-toc' || key === 'rss') { frontmatter[key] = value.toLowerCase() === 'true'; } // Parse number fields else if (key === 'sidebar-nav-prio') { frontmatter[key] = parseInt(value); } // Parse ISO timestamp fields (dates or datetimes) else if (key === 'created' || key === 'updated') { // Remove quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } frontmatter[key] = value; } // Parse tags list start else if (key === 'tags' && value === '') { // YAML list format: tags: followed by dashes frontmatter.tags = []; isInTagsList = true; } // Parse tags (comma-separated or array notation) else if (key === 'tags' && value !== '') { // Remove quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } // Remove array brackets if present if (value.startsWith('[') && value.endsWith(']')) { value = value.slice(1, -1); } // Split by comma and clean up frontmatter.tags = value.split(',').map(tag => tag.trim().replace(/['"]/g, '')).filter(Boolean); } // Ignore all other fields } } return frontmatter; } export function populateRedirects(pages) { const pageRedirects = pages.map(page => { return `${page}* ${page} 200` }).join('\n') // Add RSS feed redirect const rssRedirect = '/feed /rss.xml 301' return `${rssRedirect}\n${pageRedirects}` }