jsii-pacmak
Version:
A code generation framework for jsii backend languages
215 lines • 9.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DotNetDocGenerator = void 0;
const spec = require("@jsii/spec");
const jsii_rosetta_1 = require("jsii-rosetta");
const xmlbuilder = require("xmlbuilder");
const _utils_1 = require("../_utils");
const nameutils_1 = require("./nameutils");
const rosetta_assembly_1 = require("../../rosetta-assembly");
const type_utils_1 = require("../../type-utils");
const type_visitor_1 = require("../../type-visitor");
// Define some tokens that will be turned into literal < and > in XML comments.
// This will be used by a function later on that needs to output literal tokens,
// in a string where they would usually be escaped into < and >
//
// We use a random string in here so the actual token values cannot be predicted
// in advance, so that an attacker can not use this knowledge to inject the tokens
// literally into doc comments, and perform an XSS attack that way.
const L_ANGLE = `@l${Math.random()}@`;
const R_ANGLE = `@r${Math.random()}@`;
/**
* Generates the Jsii attributes and calls for the .NET runtime
*
* Uses the same instance of CodeMaker as the rest of the code
*/
class DotNetDocGenerator {
constructor(code, rosetta, assembly, resolver) {
this.rosetta = rosetta;
this.assembly = assembly;
this.resolver = resolver;
this.nameutils = new nameutils_1.DotNetNameUtils();
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)
*/
emitDocs(obj, apiLocation) {
const docs = obj.docs;
// The docs may be undefined at the method level but not the parameters level
this.emitXmlDoc('summary', (0, _utils_1.renderSummary)(obj.docs));
// Handling parameters only if the obj is a method
const objMethod = obj;
if (objMethod && 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(/^@/, '');
const unionHint = (0, type_utils_1.containsUnionType)(param.type)
? `Type union: ${this.renderTypeForDocs(param.type)}`
: '';
this.emitXmlDoc('param', combineSentences(param.docs?.summary, unionHint), {
attributes: { name: paramName },
});
});
}
const returnUnionHint = objMethod.returns && (0, type_utils_1.containsUnionType)(objMethod.returns.type)
? `Type union: ${this.renderTypeForDocs(objMethod.returns.type)}`
: '';
if (docs?.returns || returnUnionHint) {
this.emitXmlDoc('returns', combineSentences(docs?.returns, returnUnionHint));
}
const propUnionHint = spec.isProperty(obj) && (0, type_utils_1.containsUnionType)(obj.type)
? `Type union: ${this.renderTypeForDocs(obj.type)}`
: '';
// 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 || propUnionHint) {
this.code.line('/// <remarks>');
remarks.forEach((r) => this.code.line(`/// ${r}`.trimRight()));
if (propUnionHint) {
// Very likely to contain < and > from `Dictionary<...>`, but we also want the literal angle brackets
// from `<see cref="...">`.
this.code.line(`/// <para>${unescapeAngleMarkers(escapeAngleBrackets(propUnionHint))}</para>`);
}
this.code.line('/// </remarks>');
}
if (docs?.example) {
this.code.line('/// <example>');
this.emitXmlDoc('code', this.convertExample(docs.example, apiLocation));
this.code.line('/// </example>');
}
}
emitMarkdownAsRemarks(markdown, apiLocation) {
if (!markdown) {
return;
}
const translated = (0, jsii_rosetta_1.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}
*/
renderRemarks(docs, apiLocation) {
const ret = [];
if (docs.remarks) {
const translated = (0, jsii_rosetta_1.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, contents) {
const ls = contents.split('\n');
ret.push(`<strong>${ucFirst(name)}</strong>: ${ls[0]}`);
ret.push(...ls.slice(1));
ret.push('');
}
}
convertExample(example, apiLocation) {
(0, rosetta_assembly_1.assertSpecIsRosettaCompatible)(this.assembly);
const translated = this.rosetta.translateExample(apiLocation, example, jsii_rosetta_1.TargetLanguage.CSHARP, (0, jsii_rosetta_1.enforcesStrictMode)(this.assembly));
return translated.source;
}
convertSamplesInMarkdown(markdown, api) {
(0, rosetta_assembly_1.assertSpecIsRosettaCompatible)(this.assembly);
return this.rosetta.translateSnippetsInMarkdown(api, markdown, jsii_rosetta_1.TargetLanguage.CSHARP, (0, jsii_rosetta_1.enforcesStrictMode)(this.assembly));
}
emitXmlDoc(tag, content, { attributes = {} } = {}) {
if (!content) {
return;
}
const xml = xmlbuilder.create(tag, { headless: true }).text(content);
for (const [name, value] of Object.entries(attributes)) {
xml.att(name, value);
}
// Unescape angle brackets that may have been injected by `renderTypeForDocs`
const xmlstring = unescapeAngleMarkers(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}`);
}
}
renderTypeForDocs(x) {
return (0, type_visitor_1.visitTypeReference)(x, {
named: (ref) => `${L_ANGLE}see cref="${this.resolver.toNativeFqn(ref.fqn)}" /${R_ANGLE}`,
primitive: (ref) => this.resolver.toDotNetType(ref),
collection: (ref) => {
switch (ref.collection.kind) {
case spec.CollectionKind.Array:
return `(${this.renderTypeForDocs(ref.collection.elementtype)})[]`;
case spec.CollectionKind.Map:
return `Dictionary<string, ${this.renderTypeForDocs(ref.collection.elementtype)}>`;
}
},
union: (ref) => `either ${ref.union.types.map((x) => this.renderTypeForDocs(x)).join(' or ')}`,
intersection: (ref) => `${ref.intersection.types.map((x) => this.renderTypeForDocs(x)).join(' + ')}`,
});
}
}
exports.DotNetDocGenerator = DotNetDocGenerator;
/**
* Uppercase the first letter
*/
function ucFirst(x) {
return x.slice(0, 1).toUpperCase() + x.slice(1);
}
function shouldMentionStability(s) {
// Don't render "stable" or "external", those are both stable by implication
return s === spec.Stability.Deprecated || s === spec.Stability.Experimental;
}
function combineSentences(...xs) {
return xs.filter((x) => x).join('. ');
}
function escapeAngleBrackets(x) {
return x.replace(/</g, '<').replace(/>/g, '>');
}
/**
* Replace the special angle markers produced by renderTypeForDocs with literal angle brackets
*/
function unescapeAngleMarkers(x) {
return x
.replace(new RegExp(L_ANGLE, 'g'), '<')
.replace(new RegExp(R_ANGLE, 'g'), '>');
}
//# sourceMappingURL=dotnetdocgenerator.js.map