Skip to content

Commit 555ccc7

Browse files
Copilotmbohal
andcommitted
Implement build-time TypeScript function documentation with @microsoft/tsdoc
Co-authored-by: mbohal <4589176+mbohal@users.noreply.github.com>
1 parent e9a35b3 commit 555ccc7

14 files changed

+699
-692
lines changed

package-lock.json

Lines changed: 435 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@babel/preset-react": "^7.16.7",
4848
"@babel/preset-typescript": "^7.26.0",
4949
"@babel/register": "^7.16.9",
50+
"@microsoft/tsdoc": "^0.15.1",
5051
"@visionappscz/eslint-config-visionapps": "^1.5.0",
5152
"babel-loader": "^9.2.1",
5253
"babel-plugin-prismjs": "^2.1.0",
@@ -58,16 +59,18 @@
5859
"eslint-plugin-markdown": "^2.2.1",
5960
"eslint-plugin-react": "^7.28.0",
6061
"eslint-plugin-react-hooks": "^4.3.0",
62+
"glob": "^11.0.3",
6163
"jest": "^29.7.0",
6264
"style-loader": "^4.0.0",
6365
"terser-webpack-plugin": "^5.3.1",
66+
"typescript": "^5.8.3",
6467
"uglify-js": "^3.15.5",
6568
"webpack": "^5.66.0",
6669
"webpack-cli": "^6.0.1",
6770
"webpack-dev-server": "^5.2.0"
6871
},
6972
"scripts": {
70-
"build": "webpack --mode=production",
73+
"build": "webpack --mode=production && node scripts/processDocoffFunctionDoc.js",
7174
"prepublishOnly": "npm run build",
7275
"start": "webpack serve --mode=development",
7376
"test": "npm run test:eslint && npm run test:jest",

public/exampleTS/functions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Formats a user's full name with proper capitalization
3+
* @param firstName - The user's first name
4+
* @param lastName - The user's last name
5+
* @param includeTitle - Whether to include a title prefix
6+
* @returns The formatted full name
7+
*/
8+
export function formatName(firstName: string, lastName: string, includeTitle: boolean = false): string {
9+
const fullName = `${firstName} ${lastName}`;
10+
return includeTitle ? `Mr./Ms. ${fullName}` : fullName;
11+
}

public/index.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,22 +239,21 @@ <h3>Layout</h3>
239239

240240
<h2><code>docoff-function-doc</code></h2>
241241

242-
<p><code>&ltdocoff-function-doc&gt;</code> displays function documentation extracted from JSDoc comments in JavaScript or TypeScript files.</p>
242+
<p><code>&ltdocoff-function-doc&gt;</code> displays function documentation extracted from TSDoc comments in TypeScript files.</p>
243243

244244
<h3>Usage</h3>
245245

246246
<h4>Pure HTML</h4>
247247
<pre><code>
248-
&lt;docoff-function-doc href="/path/to/your/functions.js"&gt;&lt;/docoff-function-doc&gt;
248+
&lt;docoff-function-doc src="/path/to/your/functions.ts:functionName"&gt;&lt;/docoff-function-doc&gt;
249249
</code></pre>
250250

251251
<h3>Example</h3>
252252

253253
<p>Displaying documentation for example functions:</p>
254254

255-
<docoff-function-doc src="/exampleJS/functions.js:formatName"></docoff-function-doc>
255+
<dl><dt><strong>formatName</strong></dt><dd>Formats a user's full name with proper capitalization</dd><dt>Parameter: <code>firstName</code></dt><dd>The user's first name</dd><dt>Parameter: <code>lastName</code></dt><dd>The user's last name</dd><dt>Parameter: <code>includeTitle</code></dt><dd>Whether to include a title prefix</dd><dt>Returns:</dt><dd>The formatted full name</dd></dl>
256256

257-
</textarea>
258257
</div>
259258

260259
</body>

scripts/processDocoffFunctionDoc.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const glob = require('glob');
4+
const { TSDocParser } = require('@microsoft/tsdoc');
5+
const ts = require('typescript');
6+
7+
/**
8+
* Standalone function to process docoff-function-doc elements in HTML files
9+
* and replace them with static HTML content
10+
*/
11+
async function processDocoffFunctionDoc(options = {}) {
12+
const {
13+
sourceDir = 'public',
14+
outputDir = 'public',
15+
htmlPattern = '**/*.html',
16+
} = options;
17+
18+
console.log('Processing docoff-function-doc elements...');
19+
20+
// Find all HTML files to process
21+
const htmlFiles = glob.sync(path.join(sourceDir, htmlPattern));
22+
23+
for (const htmlFile of htmlFiles) {
24+
console.log(`Processing ${htmlFile}...`);
25+
26+
let content = fs.readFileSync(htmlFile, 'utf-8');
27+
let hasChanges = false;
28+
29+
// Find all docoff-function-doc elements
30+
const regex = /<docoff-function-doc\s+src="([^"]+)"><\/docoff-function-doc>/g;
31+
let match;
32+
33+
while ((match = regex.exec(content)) !== null) {
34+
const srcAttribute = match[1];
35+
const [filePath, functionName] = srcAttribute.split(':');
36+
37+
if (filePath && functionName) {
38+
try {
39+
const htmlContent = await generateFunctionDoc(filePath, functionName, path.dirname(htmlFile));
40+
content = content.replace(match[0], htmlContent);
41+
hasChanges = true;
42+
console.log(` Replaced docoff-function-doc for ${srcAttribute}`);
43+
} catch (error) {
44+
console.warn(` Warning: Failed to process docoff-function-doc for ${srcAttribute}: ${error.message}`);
45+
// Replace with error message
46+
const errorHtml = `<div style="color: red;">Error loading function documentation: ${error.message}</div>`;
47+
content = content.replace(match[0], errorHtml);
48+
hasChanges = true;
49+
}
50+
}
51+
}
52+
53+
if (hasChanges) {
54+
// Calculate output path
55+
const outputPath = path.resolve(outputDir, path.relative(sourceDir, htmlFile));
56+
57+
// Ensure output directory exists
58+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
59+
60+
// Write the updated content
61+
fs.writeFileSync(outputPath, content, 'utf-8');
62+
console.log(` Updated ${outputPath}`);
63+
}
64+
}
65+
66+
console.log('Finished processing docoff-function-doc elements.');
67+
}
68+
69+
async function generateFunctionDoc(filePath, functionName, baseDir) {
70+
// Resolve the absolute path to the TypeScript file
71+
const fullPath = path.resolve(baseDir, filePath.replace(/^\//, ''));
72+
73+
if (!fs.existsSync(fullPath)) {
74+
throw new Error(`File not found: ${fullPath}`);
75+
}
76+
77+
const fileContent = fs.readFileSync(fullPath, 'utf-8');
78+
79+
// Parse TypeScript file
80+
const sourceFile = ts.createSourceFile(
81+
fullPath,
82+
fileContent,
83+
ts.ScriptTarget.Latest,
84+
true
85+
);
86+
87+
// Find the specific function
88+
const functionNode = findFunctionNode(sourceFile, functionName);
89+
90+
if (!functionNode) {
91+
throw new Error(`Function '${functionName}' not found in ${filePath}`);
92+
}
93+
94+
// Extract TSDoc comment
95+
const tsdocComment = extractTSDocComment(sourceFile, functionNode);
96+
97+
if (!tsdocComment) {
98+
throw new Error(`No TSDoc comment found for function '${functionName}'`);
99+
}
100+
101+
// Parse TSDoc comment
102+
const parser = new TSDocParser();
103+
const parserContext = parser.parseString(tsdocComment);
104+
105+
if (parserContext.log.messages.length > 0) {
106+
console.warn(`TSDoc parsing warnings for ${functionName}:`, parserContext.log.messages);
107+
}
108+
109+
// Generate HTML from parsed TSDoc
110+
return generateHTMLFromTSDoc(functionName, parserContext.docComment);
111+
}
112+
113+
function findFunctionNode(sourceFile, functionName) {
114+
let functionNode = null;
115+
116+
const visit = (node) => {
117+
if (ts.isFunctionDeclaration(node) && node.name && node.name.text === functionName) {
118+
functionNode = node;
119+
return;
120+
}
121+
122+
if (ts.isVariableStatement(node)) {
123+
for (const declaration of node.declarationList.declarations) {
124+
if (ts.isIdentifier(declaration.name) && declaration.name.text === functionName) {
125+
if (declaration.initializer &&
126+
(ts.isFunctionExpression(declaration.initializer) ||
127+
ts.isArrowFunction(declaration.initializer))) {
128+
functionNode = node;
129+
return;
130+
}
131+
}
132+
}
133+
}
134+
135+
ts.forEachChild(node, visit);
136+
};
137+
138+
visit(sourceFile);
139+
return functionNode;
140+
}
141+
142+
function extractTSDocComment(sourceFile, functionNode) {
143+
// Get leading trivia (comments) for the function node
144+
const leadingTrivia = functionNode.getFullText().substring(0, functionNode.getLeadingTriviaWidth());
145+
146+
// Look for TSDoc comment (/** ... */)
147+
const tsdocRegex = /\/\*\*[\s\S]*?\*\//g;
148+
const matches = leadingTrivia.match(tsdocRegex);
149+
150+
if (matches && matches.length > 0) {
151+
// Return the last (closest) TSDoc comment
152+
return matches[matches.length - 1];
153+
}
154+
155+
return null;
156+
}
157+
158+
function generateHTMLFromTSDoc(functionName, docComment) {
159+
const summary = docComment.summarySection;
160+
const params = docComment.params;
161+
const returnsBlock = docComment.returnsBlock;
162+
163+
let html = '<dl>';
164+
165+
// Function name and description
166+
html += `<dt><strong>${functionName}</strong></dt>`;
167+
168+
if (summary && summary.nodes.length > 0) {
169+
const description = extractTextFromNodes(summary.nodes);
170+
html += `<dd>${description}</dd>`;
171+
}
172+
173+
// Parameters
174+
if (params.blocks.length > 0) {
175+
for (const param of params.blocks) {
176+
const paramName = param.parameterName;
177+
const paramDescription = param.content ? extractTextFromNodes(param.content.nodes) : '';
178+
html += `<dt>Parameter: <code>${paramName}</code></dt>`;
179+
html += `<dd>${paramDescription}</dd>`;
180+
}
181+
}
182+
183+
// Returns
184+
if (returnsBlock && returnsBlock.content) {
185+
const returnDescription = extractTextFromNodes(returnsBlock.content.nodes);
186+
html += `<dt>Returns:</dt>`;
187+
html += `<dd>${returnDescription}</dd>`;
188+
}
189+
190+
html += '</dl>';
191+
192+
return html;
193+
}
194+
195+
function extractTextFromNodes(nodes) {
196+
return nodes.map(node => {
197+
if (node.kind === 'PlainText') {
198+
return node.text;
199+
} else if (node.kind === 'Paragraph') {
200+
return extractTextFromNodes(node.nodes);
201+
} else if (node.kind === 'CodeSpan') {
202+
return `<code>${node.code}</code>`;
203+
}
204+
return '';
205+
}).join('').trim();
206+
}
207+
208+
module.exports = {
209+
processDocoffFunctionDoc,
210+
generateFunctionDoc,
211+
};
212+
213+
// If called directly from command line
214+
if (require.main === module) {
215+
processDocoffFunctionDoc().catch(console.error);
216+
}

src/DocoffFunctionDoc/DocoffFunctionDoc.js

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)