🌳
pt0/peatsite/renderGalleryAI.mjs
1import fs from 'fs'
2import path from 'path'
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)
37 }
38 return md
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>`
45 return false
46 }
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
60 if (!key) return null
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
67 const absPath = getImportMetaUrlPath(ep.importMetaUrl)
68 const relPath = absPath.replace(process.cwd() + '/', '')
69 const help = await getEpHelp(ep)
70 const action = 'help'
71 return `ptnode ${relPath} ${action}`
74const deriveEpType = (ep) => {
75 if (!ep?.importMetaUrl) return null
76 const absPath = getImportMetaUrlPath(ep.importMetaUrl)
77 const relPath = absPath.replace(process.cwd() + '/', '')
78 return getEpType(relPath)
81const extractKeyEp = (site) => {
82 const entries = Object.entries(site.epH)
83 assertSingleItem(entries, {epH: site.epH})
84 return entries[0]
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`)
98 }
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() === '---'
109 if (isBreakLine) {
110 if (currentKey) descriptions[currentKey] = currentLines.join('\n').trim()
111 sectionEndIdx = i - 1
112 break
113 }
115 if (h3Match) {
116 if (currentKey) descriptions[currentKey] = currentLines.join('\n').trim()
117 currentKey = siteKeys.includes(headingKey) ? headingKey : null
118 if (currentKey) orderedKeys.push(headingKey)
119 currentLines = []
120 sectionEndIdx = i
121 } else if (currentKey) {
122 currentLines.push(line)
123 sectionEndIdx = i
124 }
125 }
127 if (currentKey && !(currentKey in descriptions)) {
128 descriptions[currentKey] = currentLines.join('\n').trim()
129 }
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'
147 const helpOutput = await captureEpHelp(ep, { forceColor: true })
148 if (!helpOutput) return ''
149 const plainHelp = stripAnsi(helpOutput)
150 const exampleActions = getBashExamples(epName)
151 let htmlHelp = ansiToHtml(helpOutput)
152 htmlHelp = linkifyEpPath(htmlHelp, plainHelp)
153 htmlHelp = linkifyActions(htmlHelp, plainHelp, epName, exampleActions)
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>`
157 }).join('\n')
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')
165 const ossReplaceFn = await getOssReplaceFn()
166 const { forward: ossPathFwd } = await getOssPathTransformers()
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'
175 const cloneGitUrl = `https://${canonicalEnsGateway}/${ossRepoGitPath}`
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))
193 }
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)`
198 }
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
206 const epTypeDefPath = s.epType ? getEpTypeDefPath(s.epType) : null
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,
210 pkgsLine: s.appPkgNamesA.length ? jsObjStringify({deps: s.appPkgNamesA}) : 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,
215 epTypeDefPath,
216 }]
217 }))
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`
225 let href = site.href
226 if (!href) {
227 if (p) {
228 href = landingPage ? `${p}/${landingPage}` : p
229 } else {
230 const hostname = ep.nonWcHostname || ep.kube_extHostname || ep.wcHostname
231 href = hostname ? `https://${hostname}${landingPage || ''}` : helpHref
232 }
233 }
235 const command = ossTransform(site.command ?? await deriveCommand(ep)), epType = deriveEpType(ep)
236 const epTypeDefPath = getEpTypeDefPath(epType)
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}
252 }))
254 const srcBaseUrl = `/src/`
255 // Total LOC from ALL registered pt0 eps (not just gallery)
256 const allRegEps = getAllRegisteredEps().filter(p => p.startsWith('pt0/') || p.startsWith('.opencode/'))
257 const allRegStatsA = await getEpStatsForAll(allRegEps, allRegEps.map(p => getEpType(p)))
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]})
264 }
265 for (let i = 0; i < allRegStatsA.length; i++) {
266 for (const f of allRegStatsA[i].allFilesA) mergeFileEntry(f, i)
267 }
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'
271 const clr = fileClrFromEpTypes(f.inDeploy, f.inDocker, f.inFe)
272 return {...f, path: ossPathFwd(f.path), scope, clr}
273 })
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'
282 const [ptmStats] = await getEpStatsForAll([ptmEpPath], [null])
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 ?? '')
294 })
295 }
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(
298 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'}
314 }
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'))]},
319 }
320 return renderJsxToHtml(jsxPath, 'GalleryIndex', {cards, hasTile, markdownBeforeHtml, markdownAfterHtml: markdownAfterHtmlWithLoc, srcBaseUrl, totalLocStr, allAppFilesA, globalEpNamesA, epNamesA, epPathsA, epScopeLegendA, sectionLocH})