packages/jsii-pacmak/lib/targets/dotnet/dotnetdocgenerator.ts (157 lines of code) (raw):
import * as spec from '@jsii/spec';
import { CodeMaker } from 'codemaker';
import {
RosettaTabletReader,
TargetLanguage,
enforcesStrictMode,
markDownToXmlDoc,
ApiLocation,
} from 'jsii-rosetta';
import * as xmlbuilder from 'xmlbuilder';
import { renderSummary } from '../_utils';
import { DotNetNameUtils } from './nameutils';
/**
* Generates the Jsii attributes and calls for the .NET runtime
*
* Uses the same instance of CodeMaker as the rest of the code
*/
export class DotNetDocGenerator {
private readonly code: CodeMaker;
private readonly nameutils: DotNetNameUtils = new DotNetNameUtils();
public constructor(
code: CodeMaker,
private readonly rosetta: RosettaTabletReader,
private readonly assembly: spec.Assembly,
) {
this.code = code;
}
/**
* Emits all documentation depending on what is available in the jsii model
*
* Used by all kind of members + classes, interfaces, enums
* Order should be
* Summary
* Param
* Returns
* Remarks (includes examples, links, deprecated)
*/
public emitDocs(obj: spec.Documentable, apiLocation: ApiLocation): void {
const docs = obj.docs;
// The docs may be undefined at the method level but not the parameters level
this.emitXmlDoc('summary', renderSummary(obj.docs));
// Handling parameters only if the obj is a method
const objMethod = obj as spec.Method;
if (objMethod.parameters) {
objMethod.parameters.forEach((param) => {
// Remove any slug `@` from the parameter name - it's not supposed to show up here.
const paramName = this.nameutils
.convertParameterName(param.name)
.replace(/^@/, '');
this.emitXmlDoc('param', param.docs?.summary ?? '', {
attributes: { name: paramName },
});
});
}
// At this pdocfx namespacedocd a valid instance of docs
if (!docs) {
return;
}
if (docs.returns) {
this.emitXmlDoc('returns', docs.returns);
}
// Remarks does not use emitXmlDoc() because the remarks can contain code blocks
// which are fenced with <code> tags, which would be escaped to
// <code> if we used the xml builder.
const remarks = this.renderRemarks(docs, apiLocation);
if (remarks.length > 0) {
this.code.line('/// <remarks>');
remarks.forEach((r) => this.code.line(`/// ${r}`.trimRight()));
this.code.line('/// </remarks>');
}
if (docs.example) {
this.code.line('/// <example>');
this.emitXmlDoc('code', this.convertExample(docs.example, apiLocation));
this.code.line('/// </example>');
}
}
public emitMarkdownAsRemarks(
markdown: string | undefined,
apiLocation: ApiLocation,
) {
if (!markdown) {
return;
}
const translated = markDownToXmlDoc(
this.convertSamplesInMarkdown(markdown, apiLocation),
);
const lines = translated.split('\n');
this.code.line('/// <remarks>');
for (const line of lines) {
this.code.line(`/// ${line}`.trimRight());
}
this.code.line('/// </remarks>');
}
/**
* Returns the lines that should go into the <remarks> section {@link http://www.google.com|Google}
*/
private renderRemarks(docs: spec.Docs, apiLocation: ApiLocation): string[] {
const ret: string[] = [];
if (docs.remarks) {
const translated = markDownToXmlDoc(
this.convertSamplesInMarkdown(docs.remarks, apiLocation),
);
ret.push(...translated.split('\n'));
ret.push('');
}
// All the "tags" need to be rendered with empyt lines between them or they'll be word wrapped.
if (docs.default) {
emitDocAttribute('default', docs.default);
}
if (docs.stability && shouldMentionStability(docs.stability)) {
emitDocAttribute(
'stability',
this.nameutils.capitalizeWord(docs.stability),
);
}
if (docs.see) {
emitDocAttribute('see', docs.see);
}
if (docs.subclassable) {
emitDocAttribute('subclassable', '');
}
for (const [k, v] of Object.entries(docs.custom ?? {})) {
const extraSpace = k === 'link' ? ' ' : ''; // Extra space for '@link' to keep unit tests happy
emitDocAttribute(k, v + extraSpace);
}
// Remove leading and trailing empty lines
while (ret.length > 0 && ret[0] === '') {
ret.shift();
}
while (ret.length > 0 && ret[ret.length - 1] === '') {
ret.pop();
}
return ret;
function emitDocAttribute(name: string, contents: string) {
const ls = contents.split('\n');
ret.push(`<strong>${ucFirst(name)}</strong>: ${ls[0]}`);
ret.push(...ls.slice(1));
ret.push('');
}
}
private convertExample(example: string, apiLocation: ApiLocation): string {
const translated = this.rosetta.translateExample(
apiLocation,
example,
TargetLanguage.CSHARP,
enforcesStrictMode(this.assembly),
);
return translated.source;
}
private convertSamplesInMarkdown(markdown: string, api: ApiLocation): string {
return this.rosetta.translateSnippetsInMarkdown(
api,
markdown,
TargetLanguage.CSHARP,
enforcesStrictMode(this.assembly),
);
}
private emitXmlDoc(
tag: string,
content: string,
{ attributes = {} }: { attributes?: { [name: string]: string } } = {},
): void {
if (!content) {
return;
}
const xml = xmlbuilder.create(tag, { headless: true }).text(content);
for (const [name, value] of Object.entries(attributes)) {
xml.att(name, value);
}
const xmlstring = xml.end({ allowEmpty: true, pretty: false });
const trimLeft = tag !== 'code';
for (const line of xmlstring
.split('\n')
.map((x) => (trimLeft ? x.trim() : x.trimRight()))) {
this.code.line(`/// ${line}`);
}
}
}
/**
* Uppercase the first letter
*/
function ucFirst(x: string) {
return x.slice(0, 1).toUpperCase() + x.slice(1);
}
function shouldMentionStability(s: spec.Stability) {
// Don't render "stable" or "external", those are both stable by implication
return s === spec.Stability.Deprecated || s === spec.Stability.Experimental;
}