3import { fileURLToPath } from 'url' 16import { marked } from 'marked' 17import { codeToHtml } from 'shiki' 21import { readFileSync } from 'fs' 27import stripAnsi from 'strip-ansi' 29const highlightBashBlocks = async (md) => { 30 const codeBlockRegex = /```(bash|shell)\n([\s\S]*?)```/g 31 const matches = [...md.matchAll(codeBlockRegex)] 32 for (const match of matches) { 33 const [fullMatch, , code] = match 34 let html = await codeToHtml(code.trimEnd(), {lang: 'bash', theme: 'github-dark'}) 35 html = html.replace(/\n<span class="line">/g, '<span class="line">') 36 md = md.replace(fullMatch, html) 41let _ossPathFwd = (p) => p 42const srcLinkRenderer = { 43 link(href, title, text) { 44 if (href?.startsWith('pt0/') || href === 'AGENTS.md') return `<a href="${getSrcFileUrl(_ossPathFwd(href))}">${text}</a>` 48marked.use({renderer: srcLinkRenderer}) 52const __dirname = path.dirname(fileURLToPath(import.meta.url)) 53export const jsxPath = path.join(__dirname, 'GalleryIndexAI.jsx') 55const madeWPeatHeading = '## demos / [made w|deployed by] peat' 57const epHelpCache = new Map() 58const getEpHelp = async (ep) => { 59 const key = ep?.importMetaUrl 61 if (!epHelpCache.has(key)) epHelpCache.set(key, await captureEpHelp(ep)) 62 return epHelpCache.get(key) 65const deriveCommand = async (ep) => { 66 if (!ep?.importMetaUrl) return null 68 const relPath = absPath.replace(process.cwd() + '/', '') 69 const help = await getEpHelp(ep) 71 return `ptnode ${relPath} ${action}` 74const deriveEpType = (ep) => { 75 if (!ep?.importMetaUrl) return null 77 const relPath = absPath.replace(process.cwd() + '/', '') 81const extractKeyEp = (site) => { 82 const entries = Object.entries(site.epH) 87export const helpSlugForKey = (key) => `${key}-help` 89const normalizeHeading = (h) => h.replace(/^\[([^\]]+)\].*$/, '$1').trim() 91const parseSiteDescriptions = (markdown, siteKeys) => { 92 const lines = markdown.split('\n') 93 const madeWPeatIdx = lines.findIndex(l => l.trim().toLowerCase() === madeWPeatHeading) 94 if (madeWPeatIdx === -1) { 95 const preview = lines.slice(0, 30).map((l, i) => `${i}: ${l}`).join('\n') 96 console.error(`[parseSiteDescriptions] Looking for: "${madeWPeatHeading}"\nFirst 30 lines:\n${preview}`) 97 throw new Error(`Missing "${madeWPeatHeading}" heading in index.md`) 100 const descriptions = {}, orderedKeys = [] 101 let currentKey = null, currentLines = [], sectionEndIdx = madeWPeatIdx 103 for (let i = madeWPeatIdx + 1; i < lines.length; i++) { 104 const line = lines[i] 105 const h3Match = line.match(/^###\s+(.+)/) 106 const headingKey = h3Match && normalizeHeading(h3Match[1]) 107 const isBreakLine = line.match(/^##\s+/) || line.trim() === '---' 110 if (currentKey) descriptions[currentKey] = currentLines.join('\n').trim() 111 sectionEndIdx = i - 1 116 if (currentKey) descriptions[currentKey] = currentLines.join('\n').trim() 117 currentKey = siteKeys.includes(headingKey) ? headingKey : null 118 if (currentKey) orderedKeys.push(headingKey) 121 } else if (currentKey) { 122 currentLines.push(line) 127 if (currentKey && !(currentKey in descriptions)) { 128 descriptions[currentKey] = currentLines.join('\n').trim() 131 const missingKeys = siteKeys.filter(key => !(key in descriptions)) 132 if (missingKeys.length) throw new Error(`Missing site sections in index.md: ${missingKeys.join(', ')}. Found: ${Object.keys(descriptions).join(', ')}`) 134 const beforeLines = lines.slice(0, madeWPeatIdx) 135 const afterLines = lines.slice(sectionEndIdx + 1) 137 return {descriptions, orderedKeys, beforeMd: beforeLines.join('\n'), afterMd: afterLines.join('\n')} 140const helpCssSnippet = `.bash-output { white-space: pre-wrap; word-wrap: break-word; line-height: 1.4; font-family: monospace; }\n.action-link { color: var(--teal); cursor: pointer; text-decoration: underline; }\n.action-link:hover, .action-link.active { color: var(--cyan); }\n#action-output { margin-top: 24px; }` 142const buildPtmHelpBody = async (sites, ossTransform) => { 143 const ptmSite = sites.find(s => s.epH && Object.keys(s.epH)[0] === 'ptm') 144 if (!ptmSite) return '' 145 const ep = Object.values(ptmSite.epH)[0] 146 const epName = ep.epName || ep.name || 'ptm' 148 if (!helpOutput) return '' 149 const plainHelp = stripAnsi(helpOutput) 154 const templates = exampleActions.map(action => { 155 const htmlPath = path.join(bashtranscriptsDir, `${epName}-${action}.html`) 156 return `<template id="bash-${epName}-${action}">${readFileSync(htmlPath, 'utf8')}</template>` 158 return ossTransform(`<style>${transcriptCssVars}\n${helpCssSnippet}</style>\n<pre class="bash-output">${htmlHelp}</pre>\n<div id="action-output"></div>\n${templates}\n${helpPageScript}`) 161/** @param {{markdownPath?: string, peatGitSha?: string}} [opts] */ 162export const renderGalleryIndex = async (sites, opts = {}) => { 163 const {markdownPath, peatGitSha} = opts 164 const hasTile = sites.some(s => s.image === 'threerulecycle-tile') 167 _ossPathFwd = ossPathFwd 168 const ossTransform = (s) => ossReplaceFn ? ossReplaceFn(s) : s 169 const markdown = markdownPath && fs.existsSync(markdownPath) ? fs.readFileSync(markdownPath, 'utf-8') : '' 171 const siteKeys = sites.filter(s => !s.noCard).map(s => extractKeyEp(s)[0]) 172 const {descriptions, orderedKeys, beforeMd, afterMd} = parseSiteDescriptions(markdown, siteKeys) 173 const processedBeforeMd = await highlightBashBlocks(ossTransform(beforeMd)) 174 const gitShowSha = peatGitSha || 'placeholder123sha' 176 const afterMdWithSha = afterMd.replaceAll('{{cloneCmd}}', `git clone ${cloneGitUrl} && cd ${ossRepoName}\n${gitRevParseLine(gitShowSha, {ipfsNote: false})}\ncorepack enable && corepack pnpm install`) 177 const processedAfterMd = await highlightBashBlocks(ossTransform(afterMdWithSha)) 178 const cloneCardHtml = `<a href="#getting-started" class="title-clone" title="getting started"><code>git clone ${cloneGitUrl}<br>${gitOverIpfsComment}</code><span class="hamsa-flag-wrap flag-left"><img src="/Hamsa-color-scaled_720x.webp" class="hamsa-flag" alt="hamsa" /></span><span class="hamsa-flag-wrap flag-right"><img src="/standing-hanuman_720x.webp" class="hamsa-flag" alt="standing hanuman" /></span></a>` 179 const markdownBeforeHtml = marked.parse(processedBeforeMd) 180 .replace(/<pre><code>([\s\S]*?)<\/code><\/pre>/, `<div class="intro-row">${cloneCardHtml}<pre><code>$1</code></pre></div>`) 181 const markdownAfterHtml = marked.parse(processedAfterMd) 183 const sitesByKey = Object.fromEntries(sites.map(s => [extractKeyEp(s)[0], s])) 184 const orderedSites = orderedKeys.map(k => sitesByKey[k]) 186 const visibleSites = orderedSites.filter(s => !s.noCard) 187 const epPtPaths = [], epTypes = [] 188 for (const s of visibleSites) { 189 const ep = extractKeyEp(s)[1] 190 const statsPath = ep?.importMetaUrl ? getImportMetaUrlPath(ep.importMetaUrl).replace(process.cwd() + '/', '') : null 191 epPtPaths.push(statsPath) 192 epTypes.push(deriveEpType(ep)) 194 const fmtLocWithUnit = (n) => (throwIf(() => typeof n !== 'number', {n}), n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k loc` : `${n}loc`) 195 const fmtLoc = (n, total, epName, epType, hasEpTypeLoc) => { 196 const base = `(${fmtLocWithUnit(n)} uniq-to-${epName === epType ? 'ep' : epName})` 197 return hasEpTypeLoc ? base : `${base.slice(0, -1)} / ${fmtLocWithUnit(total)} importtree)` 199 const fmtEpTypeLoc = (self, total, epType) => self != null && total != null ? `( ${fmtLocWithUnit(self)} uniq-to-${epType} / ${fmtLocWithUnit(total)} importtree )` : null 200 const validIdxA = epPtPaths.map((p, i) => p ? i : null).filter(i => i !== null) 201 const rawStatsA = await getEpStatsForAll(validIdxA.map(i => epPtPaths[i]), validIdxA.map(i => epTypes[i])) 202 const locStatsByPath = Object.fromEntries(validIdxA.map((idx, i) => { 203 const s = rawStatsA[i], site = visibleSites[idx], epName = extractKeyEp(site)[0] 204 const displayLoc = site.useFullTreeLoc ? s.appLocCnt : s.appThisOnlyLocCnt 205 const hasEpTypeLoc = (s.epTypeThisOnlyLocCnt ?? 0) > 0 207 return [epPtPaths[idx], { 208 appLocStr: fmtLoc(displayLoc, s.appLocCnt, epName, s.epType, hasEpTypeLoc), 209 epTypeLocStr: hasEpTypeLoc ? fmtEpTypeLoc(s.epTypeThisOnlyLocCnt, s.epTypeLocCnt, s.epType) : null, 211 dpkgsLine: s.devPkgNamesA.length ? jsObjStringify({devDeps: s.devPkgNamesA}) : null, 212 appFilesA: s.appFilesA.map(f => ({...f, path: ossPathFwd(f.path)})), 213 epTypeFilesA: s.epTypeFilesA?.map(f => ({...f, path: ossPathFwd(f.path)})), 214 epTypeName: s.epType || null, 219 const cards = await Promise.all(visibleSites.map(async (site) => { 220 const [key, ep] = extractKeyEp(site) 221 const {path: p, landingPage, image} = site 222 const description = (descriptions[key] ? marked.parse(await highlightBashBlocks(ossTransform(descriptions[key]))) : '') + (site.descriptionHtml || '') 224 const helpHref = site.helpHref ?? `/blogposts/${helpSlugForKey(key)}/index.html` 228 href = landingPage ? `${p}/${landingPage}` : p 230 const hostname = ep.nonWcHostname || ep.kube_extHostname || ep.wcHostname 231 href = hostname ? `https://${hostname}${landingPage || ''}` : helpHref 235 const command = ossTransform(site.command ?? await deriveCommand(ep)), epType = deriveEpType(ep) 237 const epTypeHref = epTypeDefPath ? getSrcFileUrl(ossPathFwd(epTypeDefPath)) : null 238 const epPtPath = ep?.importMetaUrl ? getImportMetaUrlPath(ep.importMetaUrl).replace(process.cwd() + '/', '') : null 239 const statsKey = epPtPath 240 const locStats = statsKey ? locStatsByPath[statsKey] : null 241 href = ossTransform(href) 242 const base = {command, epType, epTypeHref, description, helpHref, locStats, collapsedGroup: site.collapsedGroup} 244 if (image === false) return {cardId: key, noTile: true, href, ...base} 245 if (image === 'threerulecycle-tile') return {cardId: key, isTile: true, path: p, href, ...base} 247 const resolvedImage = image === 'auto' ? `images/${key}-tile.png` : image 248 const isImagePath = resolvedImage && !resolvedImage.startsWith('#') 249 if (isImagePath) return {cardId: key, isImage: true, href, imageUrl: resolvedImage, imageAlign: site.imageAlign, caption: site.caption, rounds: site.rounds, ...base} 251 return {cardId: key, isTile: false, href, bgStyle: image?.startsWith('#') ? image : '#444', ...base} 254 const srcBaseUrl = `/src/` 255 // Total LOC from ALL registered pt0 eps (not just gallery) 258 const scopeBoolKeys = ['inDeploy', 'inDocker', 'inFe'] 259 const allFilesMap = new Map() 260 const mergeFileEntry = (f, epIdx) => { 261 const existing = allFilesMap.get(f.path) 262 if (existing) { existing.globalEpIndices.push(epIdx); for (const k of scopeBoolKeys) existing[k] ||= f[k] } 263 else allFilesMap.set(f.path, {...f, globalEpIndices: [epIdx]}) 265 for (let i = 0; i < allRegStatsA.length; i++) { 266 for (const f of allRegStatsA[i].allFilesA) mergeFileEntry(f, i) 268 const totalEpCnt = allRegStatsA.length 269 const allAppFilesA = [...allFilesMap.values()].map(f => { 270 const scope = f.globalEpIndices.length === 1 ? 'this' : f.globalEpIndices.length === totalEpCnt ? 'all' : 'some' 272 return {...f, path: ossPathFwd(f.path), scope, clr} 274 const totalAppLoc = allAppFilesA.reduce((sum, f) => sum + f.loc, 0) 275 const totalLocStr = `${fmtLocWithUnit(totalAppLoc)} grand total` 276 const globalEpNamesA = allRegEps.map(p => p.split('/').pop()?.replace(/^sync_/, '').replace(/\.\w+$/, '')) 277 const opencodeEpIndices = allRegEps.map((p, i) => p.startsWith('.opencode/') ? i : null).filter(i => i !== null) 278 const opencodeFilesA = allAppFilesA.filter(f => f.globalEpIndices.some(i => opencodeEpIndices.includes(i))) 279 const opencodeLocStr = `${fmtLocWithUnit(opencodeFilesA.reduce((sum, f) => sum + f.loc, 0))} importtree` 280 const opencodeEpNamesA = opencodeEpIndices.map(i => globalEpNamesA[i]) 281 const ptmEpPath = 'pt0/devpconlyF/movementsF/ptmMainAI.mts' 283 const ptmLocStr = ptmStats ? `${fmtLocWithUnit(ptmStats.appLocCnt)} importtree` : null 284 const ptmFilesA = ptmStats?.appFilesA?.map(f => ({...f, path: ossPathFwd(f.path)})) || [] 285 const ptmHelpBody = await buildPtmHelpBody(sites, ossTransform) 286 const sectionLocSpan = (key, locStr) => `<span class="total-loc" data-loc-key="${key}">${locStr}</span>` 287 /** @param {{suffix?: string, displayText?: string}} [opts] */ 288 const replaceSectionH2 = (html, pattern, id, locStr, opts = {}) => { 289 const {suffix, displayText} = opts 290 return html.replace(pattern, (_, text) => { 291 const h2 = `<h2 id="${id}">${displayText ?? text}</h2>` 292 const wrapped = locStr ? `<div class="demos-header">${h2}${sectionLocSpan(id, locStr)}</div>` : h2 293 return wrapped + (suffix ?? '') 296 const dryBadge = '<img src="dry.jpg" class="dry-img" alt="IT\'S DRY!!!" /><p class="dry-tagline">& non-rigid & consistent & ssot!</p>' 297 const markdownAfterHtmlWithSections = replaceSectionH2( 299 replaceSectionH2(markdownAfterHtml, /<h2[^>]*>(\.opencode \(optional\))<\/h2>/i, 'opencode', opencodeLocStr), 300 /<h2[^>]*>(ptm[^<]*)<\/h2>/i, 'ptm', ptmLocStr, {suffix: ptmHelpBody}), 301 /<h2[^>]*>(all\^ for a total of\.\.\.)<\/h2>/i, 'getting-started', totalLocStr, {displayText: 'getting started', suffix: dryBadge}) 302 const markdownAfterHtmlWithLoc = markdownAfterHtmlWithSections.replace( 303 /(<img src="dry\.jpg"[^>]*>)(<p class="dry-tagline">[\s\S]*?<\/p>)\s*(<pre[\s\S]*?<\/pre>)\s*<p>([\s\S]*?)<\/p>/, 304 '<div class="code-badge-grid">$1$3$2<p class="dry-first">$4</p></div>') 305 const epNamesA = validIdxA.map(idx => extractKeyEp(visibleSites[idx])[0]) 306 const epPathsA = validIdxA.map(idx => ossPathFwd(epPtPaths[idx])) 307 const epScopeLegendA = legendScopes.map(s => ({scope: s, clr: epScopeColors[s]})) 308 const resolveEpFileObj = (epPath) => { 309 const existing = allAppFilesA.find(f => f.path === epPath) 310 if (existing) return existing 311 const absPath = path.resolve(epPath) 312 const loc = fs.existsSync(absPath) ? fs.readFileSync(absPath, 'utf8').split('\n').filter(l => l.trim()).length : 0 313 return {path: epPath, loc, clr: 'yellow'} 315 const sectionLocH = { 316 'getting-started': {filesA: allAppFilesA, namesA: globalEpNamesA, epFileObjs: allRegEps.map(ossPathFwd).map(resolveEpFileObj)}, 317 opencode: {filesA: opencodeFilesA, namesA: opencodeEpNamesA, epFileObjs: opencodeEpIndices.map(i => resolveEpFileObj(ossPathFwd(allRegEps[i])))}, 318 ptm: {filesA: ptmFilesA, namesA: ['ptm'], epFileObjs: [resolveEpFileObj(ossPathFwd('pt0/path_bin/ptm'))]}, 320 return renderJsxToHtml(jsxPath, 'GalleryIndex', {cards, hasTile, markdownBeforeHtml, markdownAfterHtml: markdownAfterHtmlWithLoc, srcBaseUrl, totalLocStr, allAppFilesA, globalEpNamesA, epNamesA, epPathsA, epScopeLegendA, sectionLocH})