build-index.ts•30.7 kB
// Build pipeline step 1: Creates dist/data/index.json (bundle of all docs from submodules)
import fg from "fast-glob";
import fs from "fs/promises";
import path, { join } from "path";
import matter from "gray-matter";
interface DocEntry {
id: string; // "/sapui5/<rel-path>", "/cap/<rel-path>", "/openui5-api/<rel-path>", or "/openui5-samples/<rel-path>"
title: string;
description: string;
snippetCount: number;
relFile: string; // path relative to sources/…
type?: "markdown" | "jsdoc" | "sample" | "markdown-section"; // type of documentation
controlName?: string; // extracted UI5 control name (e.g., "Wizard", "Button")
namespace?: string; // UI5 namespace (e.g., "sap.m", "sap.f")
keywords?: string[]; // searchable keywords and tags
properties?: string[]; // control properties for API docs
events?: string[]; // control events for API docs
aggregations?: string[]; // control aggregations for API docs
parentDocument?: string; // for sections, the ID of the parent document
sectionStartLine?: number; // for sections, the line number where the section starts
headingLevel?: number; // for sections, the heading level (2=##, 3=###, 4=####)
}
interface LibraryBundle {
id: string; // "/sapui5" | "/cap" | "/openui5-api" | "/openui5-samples"
name: string; // "SAPUI5", "CAP", "OpenUI5 API", "OpenUI5 Samples"
description: string;
docs: DocEntry[];
}
interface SourceConfig {
repoName: string;
absDir: string;
id: string;
name: string;
description: string;
filePattern: string;
exclude?: string;
type: "markdown" | "jsdoc" | "sample";
}
const SOURCES: SourceConfig[] = [
{
repoName: "sapui5-docs",
absDir: join("sources", "sapui5-docs", "docs"),
id: "/sapui5",
name: "SAPUI5",
description: "Official SAPUI5 Markdown documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "cap-docs",
absDir: join("sources", "cap-docs"),
id: "/cap",
name: "SAP Cloud Application Programming Model (CAP)",
description: "CAP (Capire) reference & guides",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "openui5",
absDir: join("sources", "openui5", "src"),
id: "/openui5-api",
name: "OpenUI5 API",
description: "OpenUI5 Control API documentation and JSDoc",
filePattern: "**/src/**/*.js",
exclude: "**/test/**/*",
type: "jsdoc" as const
},
{
repoName: "openui5",
absDir: join("sources", "openui5", "src"),
id: "/openui5-samples",
name: "OpenUI5 Samples",
description: "OpenUI5 demokit sample applications and code examples",
filePattern: "**/demokit/sample/**/*.{js,xml,json,html}",
type: "sample" as const
},
{
repoName: "wdi5",
absDir: join("sources", "wdi5", "docs"),
id: "/wdi5",
name: "wdi5",
description: "wdi5 end-to-end test framework documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "ui5-tooling",
absDir: join("sources", "ui5-tooling", "docs"),
id: "/ui5-tooling",
name: "UI5 Tooling ",
description: "UI5 Tooling documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "cloud-mta-build-tool",
absDir: join("sources", "cloud-mta-build-tool", "docs", "docs"),
id: "/cloud-mta-build-tool",
name: "Cloud MTA Build Tool",
description: "Cloud MTA Build Tool documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "ui5-webcomponents",
absDir: join("sources", "ui5-webcomponents", "docs"),
id: "/ui5-webcomponents",
name: "UI5 Web Components",
description: "UI5 Web Components documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "cloud-sdk",
absDir: join("sources", "cloud-sdk", "docs-js"),
id: "/cloud-sdk-js",
name: "Cloud SDK (JavaScript)",
description: "Cloud SDK (JavaScript) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "cloud-sdk",
absDir: join("sources", "cloud-sdk", "docs-java"),
id: "/cloud-sdk-java",
name: "Cloud SDK (Java)",
description: "Cloud SDK (Java) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "cloud-sdk-ai",
absDir: join("sources", "cloud-sdk-ai", "docs-js"),
id: "/cloud-sdk-ai-js",
name: "Cloud SDK AI (JavaScript)",
description: "Cloud SDK AI (JavaScript) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "cloud-sdk-ai",
absDir: join("sources", "cloud-sdk-ai", "docs-java"),
id: "/cloud-sdk-ai-java",
name: "Cloud SDK AI (Java)",
description: "Cloud SDK AI (Java) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "ui5-typescript",
absDir: join("sources", "ui5-typescript"),
id: "/ui5-typescript",
name: "UI5 TypeScript",
description: "Official entry point to anything TypeScript related for UI5",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "ui5-cc-spreadsheetimporter",
absDir: join("sources", "ui5-cc-spreadsheetimporter", "docs"),
id: "/ui5-cc-spreadsheetimporter",
name: "UI5 CC Spreadsheet Importer",
description: "UI5 Custom Control for importing spreadsheet data",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "abap-cheat-sheets",
absDir: join("sources", "abap-cheat-sheets"),
id: "/abap-cheat-sheets",
name: "ABAP Cheat Sheets",
description: "Comprehensive ABAP syntax examples and cheat sheets",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "sap-styleguides",
absDir: join("sources", "sap-styleguides"),
id: "/sap-styleguides",
name: "SAP Style Guides",
description: "SAP coding style guides and best practices including Clean ABAP",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "dsag-abap-leitfaden",
absDir: join("sources", "dsag-abap-leitfaden", "docs"),
id: "/dsag-abap-leitfaden",
name: "DSAG ABAP Leitfaden",
description: "German ABAP guidelines and best practices by DSAG",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "abap-fiori-showcase",
absDir: join("sources", "abap-fiori-showcase"),
id: "/abap-fiori-showcase",
name: "ABAP Platform Fiori Feature Showcase",
description: "Annotation-driven SAP Fiori Elements features for OData V4 using ABAP RAP",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "cap-fiori-showcase",
absDir: join("sources", "cap-fiori-showcase"),
id: "/cap-fiori-showcase",
name: "CAP Fiori Elements Feature Showcase",
description: "SAP Fiori Elements features and annotations showcase using CAP",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.58", "md"),
id: "/abap-docs-758",
name: "ABAP Keyword Documentation (7.58)",
description: "Official ABAP language reference and syntax documentation (version 7.58) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.57", "md"),
id: "/abap-docs-757",
name: "ABAP Keyword Documentation (7.57)",
description: "Official ABAP language reference and syntax documentation (version 7.57) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.56", "md"),
id: "/abap-docs-756",
name: "ABAP Keyword Documentation (7.56)",
description: "Official ABAP language reference and syntax documentation (version 7.56) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.55", "md"),
id: "/abap-docs-755",
name: "ABAP Keyword Documentation (7.55)",
description: "Official ABAP language reference and syntax documentation (version 7.55) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.54", "md"),
id: "/abap-docs-754",
name: "ABAP Keyword Documentation (7.54)",
description: "Official ABAP language reference and syntax documentation (version 7.54) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.53", "md"),
id: "/abap-docs-753",
name: "ABAP Keyword Documentation (7.53)",
description: "Official ABAP language reference and syntax documentation (version 7.53) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.52", "md"),
id: "/abap-docs-752",
name: "ABAP Keyword Documentation (7.52)",
description: "Official ABAP language reference and syntax documentation (version 7.52) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "latest", "md"),
id: "/abap-docs-latest",
name: "ABAP Keyword Documentation (Latest)",
description: "Official ABAP language reference and syntax documentation (latest version) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
}
];
// Extract meaningful content from ABAP documentation files
function extractAbapContent(content: string, filename: string): { title: string; description: string; snippetCount: number } {
const lines = content.split(/\r?\n/);
// Skip attribution header (first few lines with "📖 Official SAP Documentation")
let contentStart = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('📖 Official SAP Documentation') || lines[i].startsWith('> **📖')) {
// Skip until we find the actual content (after attribution and separators)
for (let j = i; j < lines.length; j++) {
if (lines[j].trim() === '' || lines[j].includes('* * *') || lines[j].includes('---')) {
continue;
}
if (!lines[j].startsWith('>')) {
contentStart = j;
break;
}
}
break;
}
}
// Find the actual title (first non-metadata heading)
let title = filename.replace('.md', '').replace('aben', '');
for (let i = contentStart; i < lines.length; i++) {
const line = lines[i].trim();
if (line && !line.startsWith('AS ABAP Release') && !line.startsWith('[ABAP -') && !line.startsWith('[![') && !line.includes('Mail Feedback')) {
if (line.match(/^[A-Z][a-zA-Z\s]+$/)) {
// Found a proper title (like "Inline Declarations")
title = line;
contentStart = i + 1;
break;
}
}
}
// Extract meaningful description from content
const contentLines = lines.slice(contentStart);
const meaningfulLines = [];
for (const line of contentLines) {
const trimmed = line.trim();
// Skip empty lines, separators, and navigation
if (!trimmed || trimmed === '---' || trimmed === '* * *' || trimmed.startsWith('[ABAP -') || trimmed.includes('Mail Feedback')) {
continue;
}
// Skip metadata lines
if (trimmed.startsWith('AS ABAP Release') || trimmed.includes('©Copyright')) {
continue;
}
// Stop at "Continue" or "Programming Guideline" sections
if (trimmed.startsWith('Continue') || trimmed.startsWith('Programming Guideline')) {
break;
}
meaningfulLines.push(trimmed);
// Stop when we have enough content for a good description
if (meaningfulLines.join(' ').length > 300) {
break;
}
}
// Build description from meaningful content
let description = meaningfulLines.join(' ').trim();
// If description is too short, add version info
if (description.length < 50) {
const versionMatch = filename.match(/abap-docs-(\d+)/);
const version = versionMatch ? versionMatch[1] : '7.58';
description = `${title} - ABAP ${version} language reference`;
}
// Extract ABAP-specific terms for better searchability
const abapTerms: string[] = [];
const descriptionLower = description.toLowerCase();
// Common ABAP statement keywords
const statements = ['data', 'final', 'field-symbol', 'select', 'loop', 'if', 'try', 'catch', 'class', 'method'];
statements.forEach(stmt => {
if (descriptionLower.includes(stmt)) {
abapTerms.push(stmt);
}
});
// Add statement context if found
if (abapTerms.length > 0) {
description += ` | Statements: ${abapTerms.join(', ')}`;
}
// Count code snippets (ABAP typically has fewer but more meaningful ones)
const snippetCount = (content.match(/```/g)?.length || 0) / 2;
return {
title,
description: description.substring(0, 400), // Allow longer descriptions for ABAP
snippetCount
};
}
// Extract information from sample files (JS, XML, JSON, HTML)
function extractSampleInfo(content: string, filePath: string) {
const fileName = path.basename(filePath);
const fileExt = path.extname(filePath);
const sampleDir = path.dirname(filePath);
// Extract control name from the path (e.g., "Button", "Wizard", "Table")
const pathParts = sampleDir.split('/');
const sampleIndex = pathParts.findIndex(part => part === 'sample');
const controlName = sampleIndex >= 0 && sampleIndex < pathParts.length - 1
? pathParts[sampleIndex + 1]
: path.basename(sampleDir);
let title = `${controlName} Sample - ${fileName}`;
let description = `Sample implementation of ${controlName} control`;
let snippetCount = 0;
// Extract specific information based on file type
if (fileExt === '.js') {
// JavaScript sample files
const jsContent = content.toLowerCase();
// Look for common UI5 patterns
if (jsContent.includes('controller')) {
title = `${controlName} Sample Controller`;
description = `Controller implementation for ${controlName} sample`;
} else if (jsContent.includes('component')) {
title = `${controlName} Sample Component`;
description = `Component definition for ${controlName} sample`;
}
// Count meaningful code patterns
const codePatterns = [
/function\s*\(/g,
/onPress\s*:/g,
/on[A-Z][a-zA-Z]*\s*:/g,
/\.attach[A-Z][a-zA-Z]*/g,
/new\s+sap\./g
];
snippetCount = codePatterns.reduce((count, pattern) => {
return count + (content.match(pattern)?.length || 0);
}, 0);
} else if (fileExt === '.xml') {
// XML view files
title = `${controlName} Sample View`;
description = `XML view implementation for ${controlName} sample`;
// Count XML controls and bindings
const xmlPatterns = [
/<[a-zA-Z][^>]*>/g,
/\{[^}]+\}/g, // bindings
/press=/g,
/text=/g
];
snippetCount = xmlPatterns.reduce((count, pattern) => {
return count + (content.match(pattern)?.length || 0);
}, 0);
} else if (fileExt === '.json') {
// Manifest or model files
if (fileName.includes('manifest')) {
title = `${controlName} Sample Manifest`;
description = `Application manifest for ${controlName} sample`;
} else {
title = `${controlName} Sample Data`;
description = `Sample data model for ${controlName} control`;
}
try {
const jsonObj = JSON.parse(content);
snippetCount = Object.keys(jsonObj).length;
} catch {
snippetCount = 1;
}
} else if (fileExt === '.html') {
// HTML files
title = `${controlName} Sample HTML`;
description = `HTML page for ${controlName} sample`;
const htmlPatterns = [
/<script[^>]*>/g,
/<div[^>]*>/g,
/data-sap-ui-/g
];
snippetCount = htmlPatterns.reduce((count, pattern) => {
return count + (content.match(pattern)?.length || 0);
}, 0);
}
// Add library information from path
const libraryMatch = filePath.match(/src\/([^\/]+)\/test/);
if (libraryMatch) {
const library = libraryMatch[1];
description += ` (${library} library)`;
}
return {
title,
description,
snippetCount: Math.max(1, snippetCount) // Ensure at least 1
};
}
// Extract JSDoc information from JavaScript files with enhanced metadata
function extractJSDocInfo(content: string, fileName: string) {
const lines = content.split(/\r?\n/);
// Try to find the main class/control definition
const classMatch = content.match(/\.extend\s*\(\s*["']([^"']+)["']/);
const fullControlName = classMatch ? classMatch[1] : path.basename(fileName, ".js");
// Extract namespace and control name
const namespaceMatch = fullControlName.match(/^(sap\.[^.]+)\.(.*)/);
const namespace = namespaceMatch ? namespaceMatch[1] : '';
const controlName = namespaceMatch ? namespaceMatch[2] : fullControlName;
// Extract main class JSDoc comment
const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//);
let description = "";
if (jsdocMatch) {
// Clean up JSDoc comment and extract description
const jsdocContent = jsdocMatch[1]
.split('\n')
.map(line => line.replace(/^\s*\*\s?/, ''))
.join('\n')
.trim();
// Extract the main description (everything before @tags)
const firstAtIndex = jsdocContent.indexOf('@');
description = firstAtIndex > -1
? jsdocContent.substring(0, firstAtIndex).trim()
: jsdocContent;
// Clean up common JSDoc patterns
description = description
.replace(/^\s*Constructor for a new.*$/m, '')
.replace(/^\s*@param.*$/gm, '')
.replace(/^\s*@.*$/gm, '')
.replace(/\n\s*\n/g, '\n')
.trim();
}
// Extract properties, events, aggregations with better parsing
const properties: string[] = [];
const events: string[] = [];
const aggregations: string[] = [];
const keywords: string[] = [];
// Extract properties
const propertiesSection = content.match(/properties\s*:\s*\{([\s\S]*?)\n\s*\}/);
if (propertiesSection) {
const propMatches = propertiesSection[1].matchAll(/(\w+)\s*:\s*\{/g);
for (const match of propMatches) {
properties.push(match[1]);
}
}
// Extract events
const eventsSection = content.match(/events\s*:\s*\{([\s\S]*?)\n\s*\}/);
if (eventsSection) {
const eventMatches = eventsSection[1].matchAll(/(\w+)\s*:\s*\{/g);
for (const match of eventMatches) {
events.push(match[1]);
}
}
// Extract aggregations
const aggregationsSection = content.match(/aggregations\s*:\s*\{([\s\S]*?)\n\s*\}/);
if (aggregationsSection) {
const aggMatches = aggregationsSection[1].matchAll(/(\w+)\s*:\s*\{/g);
for (const match of aggMatches) {
aggregations.push(match[1]);
}
}
// Generate keywords based on control name and content
keywords.push(controlName.toLowerCase());
if (namespace) keywords.push(namespace);
if (fullControlName !== controlName) keywords.push(fullControlName);
// Add common UI5 control keywords based on control name
const controlLower = controlName.toLowerCase();
if (controlLower.includes('wizard')) keywords.push('wizard', 'step', 'multi-step', 'process');
if (controlLower.includes('button')) keywords.push('button', 'click', 'press', 'action');
if (controlLower.includes('table')) keywords.push('table', 'grid', 'data', 'row', 'column');
if (controlLower.includes('dialog')) keywords.push('dialog', 'popup', 'modal', 'overlay');
if (controlLower.includes('input')) keywords.push('input', 'field', 'text', 'form');
if (controlLower.includes('list')) keywords.push('list', 'item', 'collection');
if (controlLower.includes('panel')) keywords.push('panel', 'container', 'layout');
if (controlLower.includes('page')) keywords.push('page', 'navigation', 'view');
// Add property/event-based keywords
if (properties.includes('text')) keywords.push('text');
if (properties.includes('value')) keywords.push('value');
if (events.includes('press')) keywords.push('press', 'click');
if (events.includes('change')) keywords.push('change', 'update');
// Count code blocks and property definitions
const codeBlockCount = (content.match(/```/g)?.length || 0) / 2;
const propertyCount = properties.length + events.length + aggregations.length;
return {
title: fullControlName,
description: description || `OpenUI5 control: ${fullControlName}`,
snippetCount: Math.max(1, codeBlockCount + Math.floor(propertyCount / 3)),
controlName,
namespace,
keywords: [...new Set(keywords)],
properties,
events,
aggregations
};
}
function extractMarkdownSections(content: string, lines: string[], src: any, relFile: string, docs: DocEntry[]) {
const sections: { title: string; content: string; startLine: number; level: number }[] = [];
let currentSection: { title: string; content: string; startLine: number; level: number } | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for headings (##, ###, ####)
let headingLevel = 0;
let headingText = '';
if (line.startsWith('#### ')) {
headingLevel = 4;
headingText = line.slice(5).trim();
} else if (line.startsWith('### ')) {
headingLevel = 3;
headingText = line.slice(4).trim();
} else if (line.startsWith('## ')) {
headingLevel = 2;
headingText = line.slice(3).trim();
}
if (headingLevel > 0) {
// Save previous section if it exists
if (currentSection) {
sections.push(currentSection);
}
// Start new section
currentSection = {
title: headingText,
content: '',
startLine: i,
level: headingLevel
};
} else if (currentSection) {
// Add content to current section
currentSection.content += line + '\n';
}
}
// Add the last section
if (currentSection) {
sections.push(currentSection);
}
// Create separate docs entries for meaningful sections
for (const section of sections) {
// Skip very short sections or those with placeholder titles
if (section.content.trim().length < 100 || section.title.length < 3) {
continue;
}
// Generate description from section content, including code blocks for better searchability
const contentLines = section.content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
// Extract code blocks content for technical terms
const codeBlocks = section.content.match(/```[\s\S]*?```/g) || [];
const codeContent = codeBlocks
.map(block => block.replace(/```[\w]*\n?/g, '').replace(/```/g, ''))
.join(' ')
.replace(/\s+/g, ' ')
.trim();
// Combine description with code content for better indexing
let description = contentLines.slice(0, 3).join(' ').trim() || section.title;
// Include important technical terms from code blocks (like annotation qualifiers)
if (codeContent) {
// Extract meaningful technical terms (identifiers, annotation qualifiers, etc.)
const technicalTerms = (codeContent.match(/[@#]?\w+(?:\.\w+)*(?:#\w+)?/g) || [])
.filter((term: string) => term.length > 3 && !['true', 'false', 'null', 'undefined', 'function', 'return'].includes(term.toLowerCase()))
.slice(0, 10); // Limit to prevent bloating
if (technicalTerms.length > 0) {
description += ' ' + technicalTerms.join(' ');
}
}
// Count code snippets in this section
const snippetCount = (section.content.match(/```/g)?.length || 0) / 2;
// Create section entry
const sectionId = `${src.id}/${relFile.replace(/\.md$/, "")}#${section.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
docs.push({
id: sectionId,
title: section.title,
description: description.substring(0, 300) + (description.length > 300 ? '...' : ''),
snippetCount,
relFile,
type: 'markdown-section' as any,
parentDocument: `${src.id}/${relFile.replace(/\.md$/, "")}`,
sectionStartLine: section.startLine,
headingLevel: section.level
});
}
}
async function main() {
await fs.mkdir("dist/data", { recursive: true });
const all: Record<string, LibraryBundle> = {};
for (const src of SOURCES) {
const patterns = [src.filePattern];
if (src.exclude) {
patterns.push(`!${src.exclude}`);
}
const files = await fg(patterns, { cwd: src.absDir, absolute: true });
const docs: DocEntry[] = [];
for (const absPath of files) {
const rel = path.relative(src.absDir, absPath).replace(/\\/g, "/");
const raw = await fs.readFile(absPath, "utf8");
let title: string;
let description: string;
let snippetCount: number;
let id: string;
if (src.type === "markdown") {
// Handle markdown files with error handling for malformed frontmatter
let frontmatter, content;
try {
const parsed = matter(raw);
frontmatter = parsed.data;
content = parsed.content;
} catch (yamlError: any) {
console.warn(`YAML parsing failed for ${rel}, using fallback:`, yamlError?.message || yamlError);
// Fallback: extract content without frontmatter
const lines = raw.split('\n');
const contentStartIndex = lines.findIndex((line, index) => line.trim() === '---' && index > 0) + 1;
frontmatter = {};
content = contentStartIndex > 0 ? lines.slice(contentStartIndex).join('\n') : raw;
}
const lines = content.split(/\r?\n/);
// Use frontmatter for title and description (works for ABAP and other sources)
title = frontmatter?.title ||
lines.find((l) => l.startsWith("# "))?.slice(2).trim() ||
path.basename(rel, ".md");
// Enhanced description from frontmatter or content
if (frontmatter?.description) {
description = frontmatter.description;
} else if (frontmatter?.synopsis && content.includes("{{ $frontmatter.synopsis }}")) {
description = frontmatter.synopsis;
} else {
// Fallback to content extraction
const rawDescription = lines.find((l) => l.trim() && !l.startsWith("#"))?.trim() || "";
description = rawDescription;
}
snippetCount = (content.match(/```/g)?.length || 0) / 2;
id = `${src.id}/${rel.replace(/\.md$/, "")}`;
// Extract individual sections as separate entries for all markdown docs
if (content.includes('##')) {
extractMarkdownSections(content, lines, src, rel, docs);
}
} else if (src.type === "jsdoc") {
// Handle JavaScript files with JSDoc
const jsDocInfo = extractJSDocInfo(raw, path.basename(absPath));
title = jsDocInfo.title;
description = jsDocInfo.description;
snippetCount = jsDocInfo.snippetCount;
id = `${src.id}/${rel.replace(/\.js$/, "")}`;
// Skip files that don't look like UI5 controls
if (!raw.includes('.extend') || !raw.includes('metadata')) {
continue;
}
docs.push({
id,
title,
description,
snippetCount,
relFile: rel,
type: src.type,
controlName: jsDocInfo.controlName,
namespace: jsDocInfo.namespace,
keywords: jsDocInfo.keywords,
properties: jsDocInfo.properties,
events: jsDocInfo.events,
aggregations: jsDocInfo.aggregations
});
} else if (src.type === "sample") {
// Handle sample files (JS, XML, JSON, HTML)
const sampleInfo = extractSampleInfo(raw, rel);
title = sampleInfo.title;
description = sampleInfo.description;
snippetCount = sampleInfo.snippetCount;
id = `${src.id}/${rel.replace(/\.(js|xml|json|html)$/, "")}`;
// Skip empty files or non-meaningful samples
if (raw.trim().length < 50) {
continue;
}
// Extract control name from sample path for better searchability
const pathParts = rel.split('/');
const sampleIndex = pathParts.findIndex(part => part === 'sample');
const controlName = sampleIndex >= 0 && sampleIndex < pathParts.length - 1
? pathParts[sampleIndex + 1]
: path.basename(path.dirname(rel));
// Generate sample keywords
const keywords = [controlName.toLowerCase(), 'sample', 'example'];
if (rel.includes('.xml')) keywords.push('view', 'xml');
if (rel.includes('.js')) keywords.push('controller', 'javascript');
if (rel.includes('.json')) keywords.push('model', 'data', 'configuration');
if (rel.includes('manifest')) keywords.push('manifest', 'app');
docs.push({
id,
title,
description,
snippetCount,
relFile: rel,
type: src.type,
controlName,
keywords: [...new Set(keywords)]
});
} else {
continue; // Skip unknown file types
}
// For markdown files, still use the basic structure
if (src.type === "markdown") {
docs.push({
id,
title,
description,
snippetCount,
relFile: rel,
type: src.type
});
}
}
const bundle: LibraryBundle = {
id: src.id,
name: src.name,
description: src.description,
docs
};
all[src.id] = bundle;
await fs.writeFile(
path.join("dist", "data", `data${src.id}.json`.replace(/\//g, "_")),
JSON.stringify(bundle, null, 2)
);
}
await fs.writeFile("dist/data/index.json", JSON.stringify(all, null, 2));
console.log("✅ Index built with", Object.keys(all).length, "libraries.");
}
main();