Skip to content

Commit 70f3340

Browse files
authored
Merge pull request #538 from algorandfoundation/feat/add-examples-to-docs
Feat/add examples to docs
2 parents 55e61cc + 005cce6 commit 70f3340

File tree

77 files changed

+1337
-78
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1337
-78
lines changed

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
- name: Install dependencies
6161
run: npm ci
6262

63-
- name: Build package
63+
- name: Build
6464
run: npm run build
6565

6666
- name: Start LocalNet

docs/astro.config.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ export default defineConfig({
8888
collapsed: true,
8989
autogenerate: { directory: 'migration' },
9090
},
91+
{
92+
label: 'Examples',
93+
collapsed: true,
94+
items: [
95+
{ label: 'Overview', link: '/examples/' },
96+
{ label: 'ABI Encoding', link: '/examples/abi/' },
97+
{ label: 'Mnemonic Utilities', link: '/examples/algo25/' },
98+
{ label: 'Algod Client', link: '/examples/algod-client/' },
99+
{ label: 'Algorand Client', link: '/examples/algorand-client/' },
100+
{ label: 'Common Utilities', link: '/examples/common/' },
101+
{ label: 'Indexer Client', link: '/examples/indexer-client/' },
102+
{ label: 'KMD Client', link: '/examples/kmd-client/' },
103+
{ label: 'Testing', link: '/examples/testing/' },
104+
{ label: 'Transactions', link: '/examples/transact/' },
105+
],
106+
},
91107
typeDocSidebarGroup,
92108
],
93109
}),

docs/src/content.config.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
import { defineCollection } from 'astro:content';
1+
import { defineCollection, z } from 'astro:content';
22
import { docsLoader } from '@astrojs/starlight/loaders';
33
import { docsSchema } from '@astrojs/starlight/schema';
4+
import { examplesLoader } from './loaders/examples-loader';
45

56
export const collections = {
67
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
8+
examples: defineCollection({
9+
loader: examplesLoader(),
10+
schema: z.object({
11+
id: z.string(),
12+
title: z.string(),
13+
description: z.string(),
14+
prerequisites: z.string(),
15+
code: z.string(),
16+
category: z.string(),
17+
categoryLabel: z.string(),
18+
order: z.number(),
19+
filename: z.string(),
20+
runCommand: z.string(),
21+
}),
22+
}),
723
};
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import type { Loader } from 'astro/loaders'
2+
import fs from 'node:fs'
3+
import path from 'node:path'
4+
5+
type ExampleEntry = {
6+
id: string
7+
title: string
8+
description: string
9+
prerequisites: string
10+
code: string
11+
category: string
12+
categoryLabel: string
13+
order: number
14+
filename: string
15+
runCommand: string
16+
}
17+
18+
interface CategoryMeta {
19+
label: string
20+
description: string
21+
slug: string
22+
}
23+
24+
const CATEGORIES: Record<string, CategoryMeta> = {
25+
abi: {
26+
label: 'ABI Encoding',
27+
description: 'ABI type parsing, encoding, and decoding following the ARC-4 specification.',
28+
slug: 'abi',
29+
},
30+
algo25: {
31+
label: 'Mnemonic Utilities',
32+
description: 'Mnemonic and seed conversion utilities following the Algorand 25-word mnemonic standard.',
33+
slug: 'algo25',
34+
},
35+
algod_client: {
36+
label: 'Algod Client',
37+
description: 'Algorand node operations and queries using the AlgodClient.',
38+
slug: 'algod-client',
39+
},
40+
algorand_client: {
41+
label: 'Algorand Client',
42+
description: 'High-level AlgorandClient API for simplified blockchain interactions.',
43+
slug: 'algorand-client',
44+
},
45+
common: {
46+
label: 'Common Utilities',
47+
description: 'Utility functions and helpers.',
48+
slug: 'common',
49+
},
50+
indexer_client: {
51+
label: 'Indexer Client',
52+
description: 'Blockchain data queries using the IndexerClient.',
53+
slug: 'indexer-client',
54+
},
55+
kmd_client: {
56+
label: 'KMD Client',
57+
description: 'Key Management Daemon operations for wallet and key management.',
58+
slug: 'kmd-client',
59+
},
60+
testing: {
61+
label: 'Testing',
62+
description: 'Testing utilities for mock server setup and Vitest integration.',
63+
slug: 'testing',
64+
},
65+
transact: {
66+
label: 'Transactions',
67+
description: 'Low-level transaction construction and signing.',
68+
slug: 'transact',
69+
},
70+
}
71+
72+
function lineSeparator(text: string, isBullet: boolean, lastWasBullet: boolean): string {
73+
if (!text) return ''
74+
if (isBullet || lastWasBullet) return '\n'
75+
return ' '
76+
}
77+
78+
function parseJSDoc(content: string): { title: string; description: string; prerequisites: string } {
79+
const jsdocMatch = content.match(/\/\*\*\n([\s\S]*?)\*\//)
80+
81+
if (!jsdocMatch) {
82+
return { title: 'Example', description: '', prerequisites: '' }
83+
}
84+
85+
const jsdocContent = jsdocMatch[1]
86+
87+
// Extract title from "Example: Title" line
88+
const titleMatch = jsdocContent.match(/\*\s*Example:\s*(.+)/)
89+
const title = titleMatch?.[1]?.trim() || 'Example'
90+
91+
// Extract description - lines after title until Prerequisites or end
92+
const lines = jsdocContent.split('\n').map((line) => line.replace(/^\s*\*\s?/, '').trim())
93+
94+
let description = ''
95+
let prerequisites = ''
96+
let inPrerequisites = false
97+
let lastLineWasBullet = false
98+
99+
for (const line of lines) {
100+
if (line.startsWith('Example:')) continue
101+
102+
if (line.toLowerCase().startsWith('prerequisites:') || line.toLowerCase() === 'prerequisites') {
103+
inPrerequisites = true
104+
lastLineWasBullet = false
105+
const prereqContent = line.replace(/prerequisites:?\s*/i, '').trim()
106+
if (prereqContent) prerequisites = prereqContent
107+
continue
108+
}
109+
110+
if (line.startsWith('@')) continue
111+
112+
if (!line) {
113+
lastLineWasBullet = false
114+
if (inPrerequisites) {
115+
if (prerequisites) prerequisites += '\n'
116+
} else if (description) {
117+
description += '\n'
118+
}
119+
continue
120+
}
121+
122+
const isBullet = line.startsWith('-') || line.startsWith('•')
123+
if (inPrerequisites) {
124+
prerequisites += lineSeparator(prerequisites, isBullet, lastLineWasBullet) + line
125+
} else {
126+
description += lineSeparator(description, isBullet, lastLineWasBullet) + line
127+
}
128+
lastLineWasBullet = isBullet
129+
}
130+
131+
return {
132+
title,
133+
description: description.trim(),
134+
prerequisites: prerequisites.trim() || 'LocalNet running (`algokit localnet start`)',
135+
}
136+
}
137+
138+
/**
139+
* Extract order number from filename (e.g., "01-example.ts" -> 1)
140+
*/
141+
function extractOrder(filename: string): number {
142+
const match = filename.match(/^(\d+)-/)
143+
return match ? parseInt(match[1], 10) : 999
144+
}
145+
146+
function createSlug(filename: string): string {
147+
return filename.replace(/\.ts$/, '').replace(/_/g, '-')
148+
}
149+
150+
export function examplesLoader(): Loader {
151+
return {
152+
name: 'examples-loader',
153+
load: async ({ store, logger }) => {
154+
const examplesDir = path.resolve(process.cwd(), '..', 'examples')
155+
156+
logger.info(`Loading examples from ${examplesDir}`)
157+
158+
if (!fs.existsSync(examplesDir)) {
159+
logger.error(`Examples directory not found: ${examplesDir}`)
160+
return
161+
}
162+
163+
const entries: ExampleEntry[] = []
164+
165+
for (const [categoryDir, meta] of Object.entries(CATEGORIES)) {
166+
const categoryPath = path.join(examplesDir, categoryDir)
167+
168+
if (!fs.existsSync(categoryPath)) {
169+
logger.warn(`Category directory not found: ${categoryPath}`)
170+
continue
171+
}
172+
173+
const files = fs.readdirSync(categoryPath).filter((f) => f.endsWith('.ts') && !f.startsWith('_'))
174+
175+
for (const filename of files) {
176+
const filePath = path.join(categoryPath, filename)
177+
const content = fs.readFileSync(filePath, 'utf-8')
178+
const { title, description, prerequisites } = parseJSDoc(content)
179+
const order = extractOrder(filename)
180+
const slug = createSlug(filename)
181+
182+
const entry: ExampleEntry = {
183+
id: `${meta.slug}/${slug}`,
184+
title,
185+
description,
186+
prerequisites,
187+
code: content,
188+
category: categoryDir,
189+
categoryLabel: meta.label,
190+
order,
191+
filename,
192+
runCommand: `npm run example ${categoryDir}/${filename}`,
193+
}
194+
195+
entries.push(entry)
196+
}
197+
}
198+
199+
entries.sort((a, b) => {
200+
if (a.category !== b.category) {
201+
return a.category.localeCompare(b.category)
202+
}
203+
return a.order - b.order
204+
})
205+
206+
logger.info(`Found ${entries.length} examples across ${Object.keys(CATEGORIES).length} categories`)
207+
208+
for (const entry of entries) {
209+
store.set({
210+
id: entry.id,
211+
data: entry,
212+
})
213+
}
214+
},
215+
}
216+
}
217+
218+
export { CATEGORIES }
219+
export type { ExampleEntry, CategoryMeta }
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
import { getCollection } from 'astro:content'
3+
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'
4+
import { Code } from '@astrojs/starlight/components'
5+
import { CATEGORIES } from '../../loaders/examples-loader'
6+
7+
export async function getStaticPaths() {
8+
const examples = await getCollection('examples')
9+
return examples.map((example) => ({
10+
params: { slug: example.id },
11+
props: { example },
12+
}))
13+
}
14+
15+
const { example } = Astro.props
16+
const { title, description, prerequisites, code, category, categoryLabel, filename, runCommand } = example.data
17+
18+
// Get category meta for breadcrumb
19+
const categoryMeta = CATEGORIES[category]
20+
const categorySlug = categoryMeta?.slug || category
21+
22+
// Build sidebar for this example
23+
const allExamples = await getCollection('examples')
24+
const categoryExamples = allExamples
25+
.filter((e) => e.data.category === category)
26+
.sort((a, b) => a.data.order - b.data.order)
27+
28+
// GitHub source URL
29+
const githubUrl = `https://github.com/algorandfoundation/algokit-utils-ts/blob/main/examples/${category}/${filename}`
30+
---
31+
32+
<StarlightPage
33+
frontmatter={{
34+
title: title,
35+
description: description,
36+
}}
37+
headings={[
38+
{ depth: 2, text: 'Description', slug: 'description' },
39+
{ depth: 2, text: 'Prerequisites', slug: 'prerequisites' },
40+
{ depth: 2, text: 'Run This Example', slug: 'run-this-example' },
41+
{ depth: 2, text: 'Code', slug: 'code' },
42+
]}
43+
>
44+
<p><a href={`/algokit-utils-ts/examples/${categorySlug}/`}>&larr; Back to {categoryLabel}</a></p>
45+
46+
<h2 id="description">Description</h2>
47+
{(() => {
48+
const text = description || 'This example demonstrates usage of the AlgoKit Utils TypeScript library.'
49+
const lines = text.split('\n')
50+
const elements: any[] = []
51+
let bulletBuffer: string[] = []
52+
53+
const flushBullets = () => {
54+
if (bulletBuffer.length > 0) {
55+
elements.push(<ul>{bulletBuffer.map((b) => <li set:html={b} />)}</ul>)
56+
bulletBuffer = []
57+
}
58+
}
59+
60+
for (const line of lines) {
61+
const trimmed = line.trim()
62+
if (!trimmed) continue
63+
if (/^[-•]\s/.test(trimmed)) {
64+
bulletBuffer.push(trimmed.replace(/^[-•]\s*/, ''))
65+
} else {
66+
flushBullets()
67+
elements.push(<p set:html={trimmed} />)
68+
}
69+
}
70+
flushBullets()
71+
return elements
72+
})()}
73+
74+
<h2 id="prerequisites">Prerequisites</h2>
75+
<ul>
76+
{
77+
prerequisites.split('\n').map((prereq) => {
78+
const text = prereq.replace(/^[-•]\s*/, '').trim()
79+
return text ? <li set:html={text} /> : null
80+
})
81+
}
82+
</ul>
83+
84+
<h2 id="run-this-example">Run This Example</h2>
85+
<p>From the repository root:</p>
86+
<Code code={`cd examples\n${runCommand}`} lang="bash" />
87+
88+
<h2 id="code">Code</h2>
89+
<p>
90+
<a href={githubUrl} target="_blank" rel="noopener noreferrer">View source on GitHub &rarr;</a>
91+
</p>
92+
<Code code={code} lang="typescript" title={filename} />
93+
94+
<hr />
95+
96+
<h3>Other examples in {categoryLabel}</h3>
97+
<ul>
98+
{
99+
categoryExamples.map((e) => (
100+
<li>
101+
{e.id === example.id ? (
102+
<strong>{e.data.title}</strong>
103+
) : (
104+
<a href={`/algokit-utils-ts/examples/${e.id}/`}>{e.data.title}</a>
105+
)}
106+
</li>
107+
))
108+
}
109+
</ul>
110+
</StarlightPage>

0 commit comments

Comments
 (0)