feat (v1.0.0): initial refactor and redesign
This commit is contained in:
parent
3058aa1ab4
commit
fe9b50b30e
134 changed files with 17792 additions and 3670 deletions
48
lib/docs/loader.ts
Normal file
48
lib/docs/loader.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { parseTypeDocJSON } from './parser'
|
||||
import type { TypeDocRoot, DocSection } from './types'
|
||||
|
||||
/**
|
||||
* Loads and parses TypeDoc-generated API documentation from JSON file.
|
||||
*
|
||||
* @returns Array of documentation sections organized by category
|
||||
*
|
||||
* @remarks
|
||||
* This function:
|
||||
* 1. Reads the TypeDoc-generated JSON file from `public/docs/api.json`
|
||||
* 2. Parses the raw TypeDoc data into structured documentation sections
|
||||
* 3. Returns an empty array if the file is missing or invalid
|
||||
*
|
||||
* The TypeDoc JSON file should be generated by running:
|
||||
* ```bash
|
||||
* typedoc --options typedoc.json
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { loadDocumentation } from '@/lib/docs/loader'
|
||||
*
|
||||
* // In a server component
|
||||
* export default function DocsPage() {
|
||||
* const sections = loadDocumentation()
|
||||
* return <DocsList sections={sections} />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @throws Does not throw - errors are logged and an empty array is returned
|
||||
*
|
||||
* @category Documentation
|
||||
* @public
|
||||
*/
|
||||
export function loadDocumentation(): DocSection[] {
|
||||
try {
|
||||
const filePath = join(process.cwd(), 'public/docs/api.json')
|
||||
const fileContents = readFileSync(filePath, 'utf8')
|
||||
const typeDocData: TypeDocRoot = JSON.parse(fileContents)
|
||||
return parseTypeDocJSON(typeDocData)
|
||||
} catch (error) {
|
||||
console.error('Failed to load TypeDoc JSON:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
614
lib/docs/parser.ts
Normal file
614
lib/docs/parser.ts
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
/**
|
||||
* TypeDoc JSON parser that transforms TypeDoc's reflection model into a
|
||||
* simplified, searchable documentation structure.
|
||||
*
|
||||
* @remarks
|
||||
* This module parses TypeDoc JSON output (generated with `typedoc --json`)
|
||||
* and transforms it into a flattened, categorized structure optimized for:
|
||||
* - Fast client-side search
|
||||
* - Category-based navigation
|
||||
* - Rich documentation display
|
||||
*
|
||||
* **Processing pipeline:**
|
||||
* 1. Parse TypeDoc reflections recursively
|
||||
* 2. Extract JSDoc metadata (descriptions, examples, tags)
|
||||
* 3. Categorize items (Services, Utils, Types, Theme)
|
||||
* 4. Generate type signatures and function signatures
|
||||
* 5. Build navigation structure
|
||||
*
|
||||
* **Key features:**
|
||||
* - Preserves JSDoc @example blocks with language detection
|
||||
* - Filters out private/internal items
|
||||
* - Handles complex TypeScript types (unions, intersections, generics)
|
||||
* - Maintains source location references
|
||||
*
|
||||
* @module lib/docs/parser
|
||||
* @category Docs
|
||||
* @public
|
||||
*/
|
||||
|
||||
import type {
|
||||
TypeDocRoot,
|
||||
TypeDocReflection,
|
||||
TypeDocSignature,
|
||||
TypeDocParameter,
|
||||
DocItem,
|
||||
DocSection,
|
||||
DocNavigation,
|
||||
DocCategory,
|
||||
DocKind,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* Maps TypeDoc's numeric kind identifiers to our simplified DocKind types.
|
||||
*
|
||||
* @remarks
|
||||
* TypeDoc uses numeric identifiers (based on TypeScript's SymbolKind enum)
|
||||
* to represent different declaration types. This map translates them to
|
||||
* our simplified string-based kind system for easier consumption.
|
||||
*
|
||||
* **Common mappings:**
|
||||
* - 1, 128: `'class'` (Class and Constructor)
|
||||
* - 2: `'interface'`
|
||||
* - 4, 16: `'enum'` (Enum and EnumMember)
|
||||
* - 64, 512, 2048: `'function'` / `'method'`
|
||||
* - 256, 1024, 2048: `'property'`
|
||||
* - 4096, 8192, 16384: `'type'` (TypeAlias, TypeParameter)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const KIND_MAP: Record<number, DocKind> = {
|
||||
1: 'class',
|
||||
2: 'interface',
|
||||
4: 'enum',
|
||||
16: 'enum', // EnumMember
|
||||
32: 'variable',
|
||||
64: 'function',
|
||||
128: 'class', // Constructor
|
||||
256: 'property',
|
||||
512: 'method',
|
||||
1024: 'property',
|
||||
2048: 'method',
|
||||
4096: 'type',
|
||||
8192: 'type',
|
||||
16384: 'type', // TypeAlias
|
||||
65536: 'method', // CallSignature
|
||||
131072: 'method', // IndexSignature
|
||||
262144: 'method', // ConstructorSignature
|
||||
524288: 'property', // Parameter
|
||||
1048576: 'type', // TypeParameter
|
||||
2097152: 'property', // Accessor
|
||||
4194304: 'property', // GetSignature
|
||||
8388608: 'property', // SetSignature
|
||||
16777216: 'type', // ObjectLiteral
|
||||
33554432: 'type', // TypeLiteral
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses TypeDoc JSON output into categorized documentation sections.
|
||||
*
|
||||
* @param json - TypeDoc JSON root object (generated with `typedoc --json`)
|
||||
* @returns Array of documentation sections grouped by category (Services, Utils, Types, Theme, Other)
|
||||
*
|
||||
* @remarks
|
||||
* This is the main entry point for the parser. It processes the entire TypeDoc
|
||||
* reflection tree and produces a flat, categorized structure optimized for:
|
||||
* - Client-side search and filtering
|
||||
* - Category-based navigation
|
||||
* - Alphabetically sorted items within categories
|
||||
*
|
||||
* **Processing steps:**
|
||||
* 1. Recursively parse all top-level reflections
|
||||
* 2. Filter out items without descriptions or in 'Other' category
|
||||
* 3. Deduplicate items by ID
|
||||
* 4. Group by category and sort items alphabetically
|
||||
* 5. Sort sections by predefined category order
|
||||
*
|
||||
* **Category ordering:**
|
||||
* Services → Utils → Types → Theme → Other
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { parseTypeDocJSON } from '@/lib/docs/parser'
|
||||
* import typedocJson from '@/public/docs/api.json'
|
||||
*
|
||||
* const sections = parseTypeDocJSON(typedocJson)
|
||||
* // Returns: [
|
||||
* // { title: 'Services', category: 'Services', items: [...] },
|
||||
* // { title: 'Utils', category: 'Utils', items: [...] },
|
||||
* // ...
|
||||
* // ]
|
||||
* ```
|
||||
*
|
||||
* @category Docs
|
||||
* @public
|
||||
*/
|
||||
export function parseTypeDocJSON(json: TypeDocRoot): DocSection[] {
|
||||
const sections: DocSection[] = []
|
||||
const categoryMap = new Map<DocCategory, DocItem[]>()
|
||||
|
||||
if (!json.children) return sections
|
||||
|
||||
for (const child of json.children) {
|
||||
const items = parseReflection(child, undefined, true)
|
||||
for (const item of items) {
|
||||
if (item.description || item.category !== 'Other') {
|
||||
const existing = categoryMap.get(item.category) || []
|
||||
existing.push(item)
|
||||
categoryMap.set(item.category, existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [category, items] of categoryMap.entries()) {
|
||||
const uniqueItems = Array.from(
|
||||
new Map(items.map(item => [item.id, item])).values()
|
||||
)
|
||||
|
||||
sections.push({
|
||||
title: category,
|
||||
category,
|
||||
items: uniqueItems.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
})
|
||||
}
|
||||
|
||||
return sections.sort((a, b) => {
|
||||
const order = ['Services', 'Utils', 'Types', 'Theme', 'Devices', 'Domains', 'Docs', 'API', 'Other']
|
||||
return order.indexOf(a.category) - order.indexOf(b.category)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively parses a TypeDoc reflection into one or more DocItem objects.
|
||||
*
|
||||
* @param reflection - TypeDoc reflection object to parse
|
||||
* @param parentCategory - Inherited category from parent reflection
|
||||
* @param topLevel - Whether this is a top-level reflection (controls child parsing)
|
||||
* @returns Array of parsed DocItem objects
|
||||
*
|
||||
* @remarks
|
||||
* This is a recursive parsing function that handles all TypeScript declaration types.
|
||||
* It intelligently processes different reflection kinds (functions, classes, types, etc.)
|
||||
* and extracts relevant metadata.
|
||||
*
|
||||
* **Filtering:**
|
||||
* - Skips private items (isPrivate flag)
|
||||
* - Skips external items (isExternal flag)
|
||||
*
|
||||
* **Parsing strategy:**
|
||||
* - Functions with signatures → Extract parameters, returns, examples
|
||||
* - Classes/Interfaces/Types/Enums → Create item with type signature
|
||||
* - Variables/Properties → Create simple item with type
|
||||
* - Top-level items → Parse children recursively
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function parseReflection(
|
||||
reflection: TypeDocReflection,
|
||||
parentCategory?: DocCategory,
|
||||
topLevel = false
|
||||
): DocItem[] {
|
||||
const items: DocItem[] = []
|
||||
|
||||
// Skip private/internal items
|
||||
if (reflection.flags?.isPrivate || reflection.flags?.isExternal) {
|
||||
return items
|
||||
}
|
||||
|
||||
const kind = reflection.kindString
|
||||
? (reflection.kindString.toLowerCase() as DocKind)
|
||||
: KIND_MAP[reflection.kind] || 'variable'
|
||||
|
||||
const category = parentCategory || inferCategory(reflection)
|
||||
const description = extractDescription(reflection.comment)
|
||||
|
||||
if (reflection.signatures && reflection.signatures.length > 0) {
|
||||
for (const signature of reflection.signatures) {
|
||||
items.push(createDocItemFromSignature(signature, reflection, category))
|
||||
}
|
||||
}
|
||||
|
||||
else if (
|
||||
kind === 'class' ||
|
||||
kind === 'interface' ||
|
||||
kind === 'type' ||
|
||||
kind === 'enum'
|
||||
) {
|
||||
const item: DocItem = {
|
||||
id: createId(reflection),
|
||||
name: reflection.name,
|
||||
kind,
|
||||
category,
|
||||
description,
|
||||
remarks: extractRemarks(reflection.comment),
|
||||
see: extractSeeAlso(reflection.comment),
|
||||
source: extractSource(reflection),
|
||||
tags: extractTags(reflection.comment),
|
||||
deprecated: isDeprecated(reflection.comment),
|
||||
}
|
||||
|
||||
if (kind === 'type' || kind === 'interface') {
|
||||
item.signature = formatTypeSignature(reflection)
|
||||
|
||||
if (kind === 'interface' && reflection.children && reflection.children.length > 0) {
|
||||
item.parameters = reflection.children.map(child => ({
|
||||
name: child.name,
|
||||
type: child.type ? formatType(child.type) : 'any',
|
||||
description: extractDescription(child.comment),
|
||||
optional: child.flags?.isOptional || false,
|
||||
defaultValue: child.defaultValue
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
items.push(item)
|
||||
|
||||
if (topLevel && reflection.children) {
|
||||
for (const child of reflection.children) {
|
||||
items.push(...parseReflection(child, category, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else if (!parentCategory || topLevel) {
|
||||
items.push({
|
||||
id: createId(reflection),
|
||||
name: reflection.name,
|
||||
kind,
|
||||
category,
|
||||
description,
|
||||
signature: reflection.type ? formatType(reflection.type) : undefined,
|
||||
source: extractSource(reflection),
|
||||
tags: extractTags(reflection.comment),
|
||||
deprecated: isDeprecated(reflection.comment),
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete DocItem from a function/method signature with metadata.
|
||||
*
|
||||
* @param signature - TypeDoc signature containing parameters, return type, and JSDoc
|
||||
* @param parent - Parent reflection (for source location and naming)
|
||||
* @param category - Documentation category for this item
|
||||
* @returns Fully populated DocItem for a function/method
|
||||
*
|
||||
* @remarks
|
||||
* This function extracts all relevant information from a function signature including:
|
||||
* - Parameter names, types, and descriptions
|
||||
* - Return type and description
|
||||
* - Example code blocks with language identifiers
|
||||
* - JSDoc tags and deprecation status
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function createDocItemFromSignature(
|
||||
signature: TypeDocSignature,
|
||||
parent: TypeDocReflection,
|
||||
category: DocCategory
|
||||
): DocItem {
|
||||
const description = extractDescription(signature.comment)
|
||||
const parameters = signature.parameters?.map(parseParameter) || []
|
||||
const returns = signature.type
|
||||
? {
|
||||
type: formatType(signature.type),
|
||||
description: extractReturnDescription(signature.comment),
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: createId(parent),
|
||||
name: parent.name,
|
||||
kind: 'function',
|
||||
category,
|
||||
description,
|
||||
remarks: extractRemarks(signature.comment),
|
||||
signature: formatFunctionSignature(parent.name, parameters, returns?.type),
|
||||
parameters,
|
||||
returns,
|
||||
examples: extractExamples(signature.comment),
|
||||
throws: extractThrows(signature.comment),
|
||||
see: extractSeeAlso(signature.comment),
|
||||
source: extractSource(parent),
|
||||
tags: extractTags(signature.comment),
|
||||
deprecated: isDeprecated(signature.comment),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a TypeDoc parameter into a simplified parameter object.
|
||||
* @internal
|
||||
*/
|
||||
function parseParameter(param: TypeDocParameter) {
|
||||
return {
|
||||
name: param.name,
|
||||
type: param.type ? formatType(param.type) : 'any',
|
||||
description: extractDescription(param.comment),
|
||||
optional: param.flags?.isOptional || false,
|
||||
defaultValue: param.defaultValue,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract category from JSDoc @category tag
|
||||
*/
|
||||
function extractCategory(comment?: TypeDocReflection['comment']): DocCategory | undefined {
|
||||
const categoryTag = comment?.blockTags?.find((tag) => tag.tag === '@category')
|
||||
if (!categoryTag) return undefined
|
||||
|
||||
const categoryName = categoryTag.content.map((c) => c.text).join('').trim()
|
||||
|
||||
const categoryMap: Record<string, DocCategory> = {
|
||||
'Services': 'Services',
|
||||
'Utils': 'Utils',
|
||||
'Types': 'Types',
|
||||
'Theme': 'Theme',
|
||||
'Devices': 'Devices',
|
||||
'Domains': 'Domains',
|
||||
'Docs': 'Docs',
|
||||
'API': 'API',
|
||||
}
|
||||
|
||||
return categoryMap[categoryName] || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer category from reflection name and structure
|
||||
*/
|
||||
function inferCategory(reflection: TypeDocReflection): DocCategory {
|
||||
const categoryFromTag = extractCategory(reflection.comment)
|
||||
if (categoryFromTag) return categoryFromTag
|
||||
|
||||
const name = reflection.name.toLowerCase()
|
||||
|
||||
if (name.includes('service')) return 'Services'
|
||||
if (name.includes('formatter') || name.includes('util')) return 'Utils'
|
||||
if (name.includes('color') || name.includes('surface') || name.includes('theme'))
|
||||
return 'Theme'
|
||||
if (name.includes('device')) return 'Devices'
|
||||
if (name.includes('domain')) return 'Domains'
|
||||
if (reflection.kindString === 'Interface' || reflection.kindString === 'Type alias')
|
||||
return 'Types'
|
||||
|
||||
// Check source file path
|
||||
const source = reflection.sources?.[0]?.fileName
|
||||
if (source) {
|
||||
if (source.includes('/services/')) return 'Services'
|
||||
if (source.includes('/utils/')) return 'Utils'
|
||||
if (source.includes('/theme/')) return 'Theme'
|
||||
if (source.includes('/types/')) return 'Types'
|
||||
if (source.includes('/devices/')) return 'Devices'
|
||||
if (source.includes('/domains/')) return 'Domains'
|
||||
if (source.includes('/docs/')) return 'Docs'
|
||||
}
|
||||
|
||||
return 'Other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract description from TypeDoc comment
|
||||
*/
|
||||
function extractDescription(comment?: TypeDocReflection['comment']): string {
|
||||
if (!comment?.summary) return ''
|
||||
return comment.summary.map((s) => s.text).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract return description from comment
|
||||
*/
|
||||
function extractReturnDescription(comment?: TypeDocSignature['comment']): string {
|
||||
const returnTag = comment?.blockTags?.find((tag) => tag.tag === '@returns')
|
||||
if (!returnTag) return ''
|
||||
return returnTag.content.map((c) => c.text).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract remarks (extended description) from comment
|
||||
* @internal
|
||||
*/
|
||||
function extractRemarks(comment?: TypeDocReflection['comment']): string | undefined {
|
||||
const remarksTag = comment?.blockTags?.find((tag) => tag.tag === '@remarks')
|
||||
if (!remarksTag) return undefined
|
||||
return remarksTag.content.map((c) => c.text).join('').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract exception documentation from comment
|
||||
* @internal
|
||||
*/
|
||||
function extractThrows(comment?: TypeDocReflection['comment']): string[] {
|
||||
const throwsTags = comment?.blockTags?.filter((tag) => tag.tag === '@throws') || []
|
||||
return throwsTags.map((tag) => tag.content.map((c) => c.text).join('').trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract see-also references from comment
|
||||
* @internal
|
||||
*/
|
||||
function extractSeeAlso(comment?: TypeDocReflection['comment']): string[] {
|
||||
const seeTags = comment?.blockTags?.filter((tag) => tag.tag === '@see') || []
|
||||
return seeTags.map((tag) => tag.content.map((c) => c.text).join('').trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts language identifier from markdown code fences and removes fence markers.
|
||||
* @internal
|
||||
*/
|
||||
function extractCodeAndLanguage(code: string): { code: string; language: string } {
|
||||
// Extract language from opening fence (e.g., ```ts, ```tsx, ```javascript)
|
||||
const languageMatch = code.match(/^```(\w+)\n/)
|
||||
const language = languageMatch?.[1] || 'typescript'
|
||||
|
||||
// Remove opening code fence with optional language identifier
|
||||
let cleaned = code.replace(/^```(?:\w+)?\n/gm, '')
|
||||
|
||||
// Remove closing code fence
|
||||
cleaned = cleaned.replace(/\n?```$/gm, '')
|
||||
|
||||
// Trim leading/trailing whitespace
|
||||
return {
|
||||
code: cleaned.trim(),
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts example code blocks with language identifiers from TypeDoc comment tags.
|
||||
* @internal
|
||||
*/
|
||||
function extractExamples(comment?: TypeDocSignature['comment']): Array<{ code: string; language: string }> {
|
||||
const exampleTags = comment?.blockTags?.filter((tag) => tag.tag === '@example') || []
|
||||
return exampleTags.map((tag) => {
|
||||
const rawExample = tag.content.map((c) => c.text).join('')
|
||||
return extractCodeAndLanguage(rawExample)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tags from comment
|
||||
*/
|
||||
function extractTags(comment?: TypeDocReflection['comment']): string[] {
|
||||
if (!comment?.blockTags) return []
|
||||
return comment.blockTags
|
||||
.map((tag) => tag.tag.replace('@', ''))
|
||||
.filter((tag) => !['returns', 'param', 'example', 'remarks', 'throws', 'see', 'category'].includes(tag))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if item is deprecated
|
||||
*/
|
||||
function isDeprecated(comment?: TypeDocReflection['comment']): boolean {
|
||||
return comment?.blockTags?.some((tag) => tag.tag === '@deprecated') || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract source location
|
||||
*/
|
||||
function extractSource(reflection: TypeDocReflection) {
|
||||
const source = reflection.sources?.[0]
|
||||
if (!source) return undefined
|
||||
return {
|
||||
file: source.fileName,
|
||||
line: source.line,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a type to string
|
||||
*/
|
||||
function formatType(type: TypeDocReflection['type']): string {
|
||||
if (!type) return 'any'
|
||||
|
||||
switch (type.type) {
|
||||
case 'intrinsic':
|
||||
return type.name || 'any'
|
||||
case 'reference':
|
||||
return type.name || 'any'
|
||||
case 'array':
|
||||
return type.elementType ? `${formatType(type.elementType)}[]` : 'any[]'
|
||||
case 'union':
|
||||
return type.types ? type.types.map(formatType).join(' | ') : 'any'
|
||||
case 'intersection':
|
||||
return type.types ? type.types.map(formatType).join(' & ') : 'any'
|
||||
case 'literal':
|
||||
return JSON.stringify(type.value)
|
||||
case 'reflection':
|
||||
return 'object'
|
||||
default:
|
||||
return 'any'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format function signature
|
||||
*/
|
||||
function formatFunctionSignature(
|
||||
name: string,
|
||||
parameters: Array<{
|
||||
name: string
|
||||
type: string
|
||||
optional: boolean
|
||||
defaultValue?: string
|
||||
}>,
|
||||
returnType?: string
|
||||
): string {
|
||||
const params = parameters
|
||||
.map((p) => {
|
||||
const opt = p.optional ? '?' : ''
|
||||
const def = p.defaultValue ? ` = ${p.defaultValue}` : ''
|
||||
return `${p.name}${opt}: ${p.type}${def}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return `${name}(${params})${returnType ? `: ${returnType}` : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format type signature for interfaces/types
|
||||
*/
|
||||
function formatTypeSignature(reflection: TypeDocReflection): string {
|
||||
if (!reflection.type) return ''
|
||||
|
||||
const type = reflection.type
|
||||
if (type.type === 'reflection' && type.declaration?.children) {
|
||||
const props = type.declaration.children
|
||||
.map((child) => {
|
||||
const opt = child.flags?.isOptional ? '?' : ''
|
||||
const childType = child.type ? formatType(child.type) : 'any'
|
||||
return ` ${child.name}${opt}: ${childType}`
|
||||
})
|
||||
.join('\n')
|
||||
return `{\n${props}\n}`
|
||||
}
|
||||
|
||||
return formatType(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique ID for a doc item
|
||||
*/
|
||||
function createId(reflection: TypeDocReflection): string {
|
||||
const source = reflection.sources?.[0]
|
||||
if (source) {
|
||||
const file = source.fileName.replace(/^.*\/(lib|components)\//, '')
|
||||
return `${file}-${reflection.name}-${reflection.id}`
|
||||
}
|
||||
return `${reflection.name}-${reflection.id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build navigation structure from doc sections
|
||||
*/
|
||||
export function buildNavigation(sections: DocSection[]): DocNavigation {
|
||||
return {
|
||||
sections: sections.map((section) => ({
|
||||
title: section.title,
|
||||
category: section.category,
|
||||
items: section.items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
kind: item.kind,
|
||||
})),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all doc items flattened
|
||||
*/
|
||||
export function getAllItems(sections: DocSection[]): DocItem[] {
|
||||
return sections.flatMap((section) => section.items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a doc item by ID
|
||||
*/
|
||||
export function findItemById(sections: DocSection[], id: string): DocItem | undefined {
|
||||
for (const section of sections) {
|
||||
const item = section.items.find((i) => i.id === id)
|
||||
if (item) return item
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
344
lib/docs/search.ts
Normal file
344
lib/docs/search.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
/**
|
||||
* Documentation search engine with weighted scoring algorithm.
|
||||
*
|
||||
* @remarks
|
||||
* This module provides fast, client-side search functionality for documentation items
|
||||
* with a sophisticated scoring system that prioritizes different types of matches.
|
||||
*
|
||||
* **Features:**
|
||||
* - Multi-term search with space-separated queries
|
||||
* - Weighted scoring (exact matches > prefix matches > contains matches)
|
||||
* - Category and kind filtering
|
||||
* - Tag-based filtering
|
||||
* - Search suggestions based on partial queries
|
||||
* - Results grouping by category
|
||||
*
|
||||
* **Scoring system:**
|
||||
* - Exact name match: 100 points
|
||||
* - Name starts with term: 50 points
|
||||
* - Name contains term: 30 points
|
||||
* - Description contains term: 20 points
|
||||
* - Signature contains term: 15 points
|
||||
* - Tag contains term: 10 points
|
||||
* - Parameter name contains term: 5 points
|
||||
*
|
||||
* @module lib/docs/search
|
||||
* @category Docs
|
||||
* @public
|
||||
*/
|
||||
|
||||
import type { DocItem, DocFilters, APIEndpoint } from './types'
|
||||
|
||||
/**
|
||||
* Searches through documentation items with filtering and scoring.
|
||||
*
|
||||
* @param items - Array of documentation items to search
|
||||
* @param query - Search query string (space-separated terms)
|
||||
* @param filters - Optional filters for category, kind, and tags
|
||||
* @returns Filtered and scored array of documentation items, sorted by relevance
|
||||
*
|
||||
* @remarks
|
||||
* This function implements a two-phase search:
|
||||
* 1. **Filter phase**: Apply category, kind, and tag filters
|
||||
* 2. **Search phase**: Score items based on query term matches
|
||||
*
|
||||
* **Empty query handling:**
|
||||
* If query is empty or only whitespace, returns filtered items without scoring.
|
||||
*
|
||||
* **Multi-term queries:**
|
||||
* Space-separated terms are searched independently and scores are accumulated.
|
||||
* Example: "format date" searches for both "format" AND "date".
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { searchDocs } from '@/lib/docs/search'
|
||||
* import { getAllItems } from '@/lib/docs/parser'
|
||||
*
|
||||
* const allItems = getAllItems(sections)
|
||||
*
|
||||
* // Simple search
|
||||
* const results = searchDocs(allItems, 'formatter')
|
||||
*
|
||||
* // Search with filters
|
||||
* const serviceResults = searchDocs(allItems, 'get domain', {
|
||||
* category: 'Services',
|
||||
* kind: 'function'
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @category Docs
|
||||
* @public
|
||||
*/
|
||||
export function searchDocs(
|
||||
items: DocItem[],
|
||||
query: string,
|
||||
filters?: DocFilters
|
||||
): DocItem[] {
|
||||
let results = items
|
||||
|
||||
// Apply filters
|
||||
if (filters) {
|
||||
if (filters.category) {
|
||||
results = results.filter((item) => item.category === filters.category)
|
||||
}
|
||||
if (filters.kind) {
|
||||
results = results.filter((item) => item.kind === filters.kind)
|
||||
}
|
||||
if (filters.tags && filters.tags.length > 0) {
|
||||
results = results.filter((item) =>
|
||||
filters.tags!.some((tag) => item.tags?.includes(tag))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply search query
|
||||
if (!query || query.trim() === '') {
|
||||
return results
|
||||
}
|
||||
|
||||
const searchTerms = query.toLowerCase().split(/\s+/)
|
||||
|
||||
return results
|
||||
.map((item) => ({
|
||||
item,
|
||||
score: calculateSearchScore(item, searchTerms),
|
||||
}))
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map(({ item }) => item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates weighted search score for a documentation item.
|
||||
* @internal
|
||||
*/
|
||||
function calculateSearchScore(item: DocItem, searchTerms: string[]): number {
|
||||
let score = 0
|
||||
const name = item.name.toLowerCase()
|
||||
const description = item.description.toLowerCase()
|
||||
const signature = item.signature?.toLowerCase() || ''
|
||||
const tags = (item.tags || []).join(' ').toLowerCase()
|
||||
|
||||
for (const term of searchTerms) {
|
||||
// Exact name match (highest score)
|
||||
if (name === term) {
|
||||
score += 100
|
||||
continue
|
||||
}
|
||||
|
||||
// Name starts with term
|
||||
if (name.startsWith(term)) {
|
||||
score += 50
|
||||
continue
|
||||
}
|
||||
|
||||
// Name contains term
|
||||
if (name.includes(term)) {
|
||||
score += 30
|
||||
continue
|
||||
}
|
||||
|
||||
// Description contains term
|
||||
if (description.includes(term)) {
|
||||
score += 20
|
||||
}
|
||||
|
||||
// Signature contains term
|
||||
if (signature.includes(term)) {
|
||||
score += 15
|
||||
}
|
||||
|
||||
// Tags contain term
|
||||
if (tags.includes(term)) {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// Parameter names contain term
|
||||
if (item.parameters) {
|
||||
for (const param of item.parameters) {
|
||||
if (param.name.toLowerCase().includes(term)) {
|
||||
score += 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches through API endpoints with weighted scoring.
|
||||
*
|
||||
* @param endpoints - Array of API endpoints to search
|
||||
* @param query - Search query string (space-separated terms)
|
||||
* @returns Filtered and scored array of API endpoints, sorted by relevance
|
||||
*
|
||||
* @remarks
|
||||
* Similar to searchDocs but optimized for API endpoint structure.
|
||||
* Searches path, method, and description fields.
|
||||
*
|
||||
* @category Docs
|
||||
* @public
|
||||
*/
|
||||
export function searchAPIs(
|
||||
endpoints: APIEndpoint[],
|
||||
query: string
|
||||
): APIEndpoint[] {
|
||||
if (!query || query.trim() === '') {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
const searchTerms = query.toLowerCase().split(/\s+/)
|
||||
|
||||
return endpoints
|
||||
.map((endpoint) => ({
|
||||
endpoint,
|
||||
score: calculateAPIScore(endpoint, searchTerms),
|
||||
}))
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map(({ endpoint }) => endpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate search score for API endpoint
|
||||
*/
|
||||
function calculateAPIScore(endpoint: APIEndpoint, searchTerms: string[]): number {
|
||||
let score = 0
|
||||
const path = endpoint.path.toLowerCase()
|
||||
const description = endpoint.description.toLowerCase()
|
||||
const method = endpoint.method.toLowerCase()
|
||||
|
||||
for (const term of searchTerms) {
|
||||
// Path exact match
|
||||
if (path === term) {
|
||||
score += 100
|
||||
continue
|
||||
}
|
||||
|
||||
// Path contains term
|
||||
if (path.includes(term)) {
|
||||
score += 50
|
||||
}
|
||||
|
||||
// Method matches
|
||||
if (method === term) {
|
||||
score += 40
|
||||
}
|
||||
|
||||
// Description contains term
|
||||
if (description.includes(term)) {
|
||||
score += 20
|
||||
}
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search suggestions based on partial query
|
||||
*/
|
||||
export function getSearchSuggestions(
|
||||
items: DocItem[],
|
||||
query: string,
|
||||
limit = 5
|
||||
): string[] {
|
||||
if (!query || query.trim() === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
const queryLower = query.toLowerCase()
|
||||
const suggestions = new Set<string>()
|
||||
|
||||
for (const item of items) {
|
||||
// Suggest item names
|
||||
if (item.name.toLowerCase().includes(queryLower)) {
|
||||
suggestions.add(item.name)
|
||||
}
|
||||
|
||||
// Suggest tags
|
||||
if (item.tags) {
|
||||
for (const tag of item.tags) {
|
||||
if (tag.toLowerCase().includes(queryLower)) {
|
||||
suggestions.add(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestions.size >= limit) break
|
||||
}
|
||||
|
||||
return Array.from(suggestions).slice(0, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Group search results by category
|
||||
*/
|
||||
export function groupByCategory(items: DocItem[]): Map<string, DocItem[]> {
|
||||
const grouped = new Map<string, DocItem[]>()
|
||||
|
||||
for (const item of items) {
|
||||
const category = item.category
|
||||
const existing = grouped.get(category) || []
|
||||
existing.push(item)
|
||||
grouped.set(category, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight search terms in text
|
||||
*/
|
||||
export function highlightSearchTerms(
|
||||
text: string,
|
||||
searchTerms: string[]
|
||||
): string {
|
||||
let highlighted = text
|
||||
|
||||
for (const term of searchTerms) {
|
||||
const regex = new RegExp(`(${escapeRegExp(term)})`, 'gi')
|
||||
highlighted = highlighted.replace(regex, '<mark>$1</mark>')
|
||||
}
|
||||
|
||||
return highlighted
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special regex characters
|
||||
*/
|
||||
function escapeRegExp(text: string): string {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create search index for faster lookups
|
||||
*/
|
||||
export function createSearchIndex(items: DocItem[]): Map<string, DocItem[]> {
|
||||
const index = new Map<string, DocItem[]>()
|
||||
|
||||
for (const item of items) {
|
||||
// Index by name tokens
|
||||
const nameTokens = item.name.toLowerCase().split(/[_\-\s]+/)
|
||||
for (const token of nameTokens) {
|
||||
const existing = index.get(token) || []
|
||||
if (!existing.includes(item)) {
|
||||
existing.push(item)
|
||||
index.set(token, existing)
|
||||
}
|
||||
}
|
||||
|
||||
// Index by tags
|
||||
if (item.tags) {
|
||||
for (const tag of item.tags) {
|
||||
const existing = index.get(tag.toLowerCase()) || []
|
||||
if (!existing.includes(item)) {
|
||||
existing.push(item)
|
||||
index.set(tag.toLowerCase(), existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
225
lib/docs/types.ts
Normal file
225
lib/docs/types.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* Type definitions for documentation system
|
||||
*/
|
||||
|
||||
export interface TypeDocReflection {
|
||||
id: number
|
||||
name: string
|
||||
kind: number
|
||||
kindString?: string
|
||||
flags?: {
|
||||
isExported?: boolean
|
||||
isExternal?: boolean
|
||||
isOptional?: boolean
|
||||
isRest?: boolean
|
||||
isPrivate?: boolean
|
||||
isProtected?: boolean
|
||||
isPublic?: boolean
|
||||
isStatic?: boolean
|
||||
isReadonly?: boolean
|
||||
isAbstract?: boolean
|
||||
}
|
||||
comment?: {
|
||||
summary?: Array<{ kind: string; text: string }>
|
||||
blockTags?: Array<{
|
||||
tag: string
|
||||
content: Array<{ kind: string; text: string }>
|
||||
}>
|
||||
}
|
||||
children?: TypeDocReflection[]
|
||||
groups?: Array<{
|
||||
title: string
|
||||
children: number[]
|
||||
}>
|
||||
sources?: Array<{
|
||||
fileName: string
|
||||
line: number
|
||||
character: number
|
||||
}>
|
||||
signatures?: TypeDocSignature[]
|
||||
type?: TypeDocType
|
||||
defaultValue?: string
|
||||
parameters?: TypeDocParameter[]
|
||||
}
|
||||
|
||||
export interface TypeDocSignature {
|
||||
id: number
|
||||
name: string
|
||||
kind: number
|
||||
kindString?: string
|
||||
comment?: {
|
||||
summary?: Array<{ kind: string; text: string }>
|
||||
blockTags?: Array<{
|
||||
tag: string
|
||||
content: Array<{ kind: string; text: string }>
|
||||
}>
|
||||
}
|
||||
parameters?: TypeDocParameter[]
|
||||
type?: TypeDocType
|
||||
}
|
||||
|
||||
export interface TypeDocParameter {
|
||||
id: number
|
||||
name: string
|
||||
kind: number
|
||||
kindString?: string
|
||||
flags?: {
|
||||
isOptional?: boolean
|
||||
isRest?: boolean
|
||||
}
|
||||
comment?: {
|
||||
summary?: Array<{ kind: string; text: string }>
|
||||
}
|
||||
type?: TypeDocType
|
||||
defaultValue?: string
|
||||
}
|
||||
|
||||
export interface TypeDocType {
|
||||
type: string
|
||||
name?: string
|
||||
value?: string | number | boolean | null
|
||||
types?: TypeDocType[]
|
||||
typeArguments?: TypeDocType[]
|
||||
elementType?: TypeDocType
|
||||
declaration?: TypeDocReflection
|
||||
target?: number
|
||||
package?: string
|
||||
qualifiedName?: string
|
||||
}
|
||||
|
||||
export interface TypeDocRoot {
|
||||
id: number
|
||||
name: string
|
||||
kind: number
|
||||
kindString?: string
|
||||
children?: TypeDocReflection[]
|
||||
groups?: Array<{
|
||||
title: string
|
||||
children: number[]
|
||||
}>
|
||||
packageName?: string
|
||||
packageVersion?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Processed documentation structure
|
||||
*/
|
||||
export interface DocItem {
|
||||
id: string
|
||||
name: string
|
||||
kind: DocKind
|
||||
category: DocCategory
|
||||
description: string
|
||||
remarks?: string
|
||||
signature?: string
|
||||
parameters?: DocParameter[]
|
||||
returns?: {
|
||||
type: string
|
||||
description: string
|
||||
}
|
||||
examples?: Array<{
|
||||
code: string
|
||||
language: string
|
||||
}>
|
||||
throws?: string[]
|
||||
see?: string[]
|
||||
source?: {
|
||||
file: string
|
||||
line: number
|
||||
}
|
||||
tags?: string[]
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export type DocKind =
|
||||
| 'function'
|
||||
| 'method'
|
||||
| 'class'
|
||||
| 'interface'
|
||||
| 'type'
|
||||
| 'variable'
|
||||
| 'property'
|
||||
| 'enum'
|
||||
|
||||
export type DocCategory = 'Services' | 'Utils' | 'Types' | 'Theme' | 'Devices' | 'Domains' | 'Docs' | 'API' | 'Other'
|
||||
|
||||
export interface DocParameter {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
optional: boolean
|
||||
defaultValue?: string
|
||||
}
|
||||
|
||||
export interface DocSection {
|
||||
title: string
|
||||
items: DocItem[]
|
||||
category: DocCategory
|
||||
}
|
||||
|
||||
export interface DocNavigation {
|
||||
sections: Array<{
|
||||
title: string
|
||||
category: DocCategory
|
||||
items: Array<{
|
||||
id: string
|
||||
name: string
|
||||
kind: DocKind
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint documentation
|
||||
*/
|
||||
export interface APIEndpoint {
|
||||
id: string
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||
path: string
|
||||
description: string
|
||||
category: string
|
||||
auth?: {
|
||||
required: boolean
|
||||
type?: string
|
||||
description?: string
|
||||
}
|
||||
parameters?: {
|
||||
query?: DocParameter[]
|
||||
body?: DocParameter[]
|
||||
headers?: DocParameter[]
|
||||
}
|
||||
responses: Array<{
|
||||
status: number
|
||||
description: string
|
||||
schema?: Record<string, unknown>
|
||||
example?: Record<string, unknown>
|
||||
}>
|
||||
examples?: Array<{
|
||||
title: string
|
||||
request: string | Record<string, unknown>
|
||||
response: string | Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Search result
|
||||
*/
|
||||
export interface SearchResult {
|
||||
item: DocItem | APIEndpoint
|
||||
matches: Array<{
|
||||
key: string
|
||||
value: string
|
||||
indices: Array<[number, number]>
|
||||
}>
|
||||
score: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Documentation filters
|
||||
*/
|
||||
export interface DocFilters {
|
||||
category?: DocCategory
|
||||
kind?: DocKind
|
||||
search?: string
|
||||
tags?: string[]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue