UNPKG

jsii-pacmak

Version:

A code generation framework for jsii backend languages

215 lines 9.6 kB
"use strict"; 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 &lt; and &gt; // // 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 // &lt;code&gt; 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, '&lt;').replace(/>/g, '&gt;'); } /** * 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