/*# # docs Utility for extracting documentation from markdown files and inline comments in source code. > `docs.ts` is intended to be run directly using `bun`. You can transpile it to javascript if you want to run it using node. This is used by the `doc-browser` component to build searchable, navigable documentation from your project's source files. ## Usage import { extractDocs } from 'docs' extractDocs({ paths: ['src', 'README.md'], ignore: ['node_modules', 'dist', 'build'] path: 'public/docs.json' }) ## API ### `extractDocs(options)` Scans directories for markdown files and source code comments. **Options:** - `paths`: Array of directory paths or file paths to scan - `ignore`: Array of directory names to ignore (default: ['node_modules', 'dist']) - `output`: if provided, path to write json result. **Returns:** Array of `Doc` objects ### `Doc` object structure { text: string, // Markdown content title: string, // First heading or filename filename: string, // Just the filename path: string, // Full file path pin?: 'top' | 'bottom' // Optional pinning for sort order } ## Documentation Format ### Markdown files Any `.md` file will be included in its entirety. ### Source code comments Multi-line comments that start with `/*#` will be extracted as markdown: /*# # My Component This is documentation for my component. ```html ``` ```js console.log('hello world') ``` ```css my-componet { color: blue } ``` *‎/ export class MyComponent extends Component { // implementation } ... The [doc-browser](/?doc-browser.ts) will render the output as a test-bed project with documentation and live examples. ### Metadata You can include JSON metadata in comments to control sorting: html: ts, js, css: /*{ "pin": "bottom" }*‎/ This will pin the document to the top or bottom of the navigation list. */ /*{ "pin": "bottom" }*/ // TODO CLI options import * as fs from 'fs' import * as path from 'path' export interface Doc { text: string title: string filename: string path: string pin?: 'top' | 'bottom' hidden?: boolean } export interface ExtractDocsOptions { paths: string[] ignore?: string[] output?: string } const TRIM_REGEX = /^#+ |`/g function metadata(content: string, filePath: string): Partial { const source = content.match(/<\!\-\-(\{.*\})\-\->|\/\*(\{.*\})\*\//) let data: Partial = {} if (source) { try { data = JSON.parse(source[1] || source[2]) } catch (e) { console.error('bad metadata in doc', filePath) } } return data } function pinnedSort(a: Doc, b: Doc): number { const aKey = (a.pin === 'top' ? 'A' : a.pin === 'bottom' ? 'Z' : 'M') + a.title.toLocaleLowerCase() const bKey = (b.pin === 'top' ? 'A' : b.pin === 'bottom' ? 'Z' : 'M') + b.title.toLocaleLowerCase() return aKey > bKey ? 1 : bKey > aKey ? -1 : 0 } function findMarkdownFiles(paths: string[], ignore: string[]): Doc[] { const markdownFiles: Doc[] = [] function traverseDirectory(dir: string, ignore: string[]) { console.log(dir) const files = fs.readdirSync(dir) const baseName = path.basename(dir) if (ignore.includes(baseName)) { return } files.forEach((file) => { const filePath = path.join(dir, file) let stats try { stats = fs.statSync(filePath) } catch (err) { return } if (stats.isDirectory()) { traverseDirectory(filePath, ignore) } else if (path.extname(file) === '.md') { const content = fs.readFileSync(filePath, 'utf8') markdownFiles.push({ text: content, title: content.split('\n')[0].replace(TRIM_REGEX, ''), filename: file, path: filePath, ...metadata(content, filePath), }) } else if (['.ts', '.js', '.css'].includes(path.extname(file))) { const content = fs.readFileSync(filePath, 'utf8') const docs = content.match(/\/\*#[\s\S]+?\*\//g) || [] if (docs.length) { const markdown = docs.map((s) => s.substring(3, s.length - 2).trim()) const text = markdown.join('\n\n') markdownFiles.push({ text, title: text.split('\n')[0].replace(TRIM_REGEX, ''), filename: file, path: filePath, ...metadata(content, filePath), }) } } }) } paths.forEach((dir) => { try { const stats = fs.statSync(dir) if (stats.isDirectory()) { traverseDirectory(dir, ignore) } else if (stats.isFile()) { const file = path.basename(dir) if (path.extname(file) === '.md') { const content = fs.readFileSync(dir, 'utf8') markdownFiles.push({ text: content, title: content.split('\n')[0].replace(TRIM_REGEX, ''), filename: file, path: dir, ...metadata(content, dir), }) } } } catch (err) { console.error(`Could not read ${dir}:`, err) } }) return markdownFiles.sort(pinnedSort) } export function extractDocs(options: ExtractDocsOptions): Doc[] { const { paths, ignore = ['node_modules', 'dist', 'build', 'docs'], output, } = options const docs = findMarkdownFiles(paths, ignore) if (output) { saveDocsJSON(docs, output) } return docs } export function saveDocsJSON(docs: Doc[], outputPath: string): void { const jsonData = JSON.stringify(docs, null, 2) fs.writeFileSync(outputPath, jsonData, 'utf8') console.log(`Documentation saved to ${outputPath}`) }