diff --git a/README.md b/README.md index c187387a4..36ba54ceb 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Extended version of [https://github.com/xiag-ag/typescript-to-json-schema](https Inspired by [`YousefED/typescript-json-schema`](https://github.com/YousefED/typescript-json-schema). Here's the differences list: -- this implementation avoids the use of `typeChecker.getTypeAtLocation()` (so probably it keeps correct type aliases) -- processing AST and formatting JSON schema have been split into two independent steps -- not exported types, interfaces, enums are not exposed in the `definitions` section in the JSON schema +- this implementation avoids the use of `typeChecker.getTypeAtLocation()` (so probably it keeps correct type aliases) +- processing AST and formatting JSON schema have been split into two independent steps +- not exported types, interfaces, enums are not exposed in the `definitions` section in the JSON schema ## Contributors @@ -47,6 +47,7 @@ By default, the command-line generator will use the `tsconfig.json` file in the -e, --expose Type exposing (choices: "all", "none", "export", default: "export") -j, --jsDoc Read JsDoc annotations (choices: "none", "basic", "extended", default: "extended") --markdown-description Generate `markdownDescription` in addition to `description`. + --full-description Include the full raw JSDoc comment as `fullDescription` in the schema. --functions How to handle functions. `fail` will throw an error. `comment` will add a comment. `hide` will treat the function like a NeverType or HiddenType. (choices: "fail", "comment", "hide", default: "comment") --minify Minify generated schema (default: false) @@ -221,20 +222,20 @@ fs.writeFile(outputPath, schemaString, (err) => { ## Current state -- `interface` types -- `enum` types -- `union`, `tuple`, `type[]` types -- `Date`, `RegExp`, `URL` types -- `string`, `boolean`, `number` types -- `"value"`, `123`, `true`, `false`, `null`, `undefined` literals -- type aliases -- generics -- `typeof` -- `keyof` -- conditional types -- functions -- `Promise` unwraps to `T` -- Overrides (like `@format`) +- `interface` types +- `enum` types +- `union`, `tuple`, `type[]` types +- `Date`, `RegExp`, `URL` types +- `string`, `boolean`, `number` types +- `"value"`, `123`, `true`, `false`, `null`, `undefined` literals +- type aliases +- generics +- `typeof` +- `keyof` +- conditional types +- functions +- `Promise` unwraps to `T` +- Overrides (like `@format`) ## Run locally @@ -252,7 +253,7 @@ And connect via the debugger protocol. Publishing is handled by a 2-branch [pre-release process](https://intuit.github.io/auto/docs/generated/shipit#next-branch-default), configured in `publish-auto.yml`. All changes should be based off the default `next` branch, and are published automatically. -- PRs made into the default branch are auto-deployed to the `next` pre-release tag on NPM. The result can be installed with `npm install ts-json-schema-generator@next` - - When merging into `next`, please use the `squash and merge` strategy. -- To release a new stable version, open a PR from `next` into `stable` using this [compare link](https://github.com/vega/ts-json-schema-generator/compare/stable...next). - - When merging from `next` into `stable`, please use the `create a merge commit` strategy. +- PRs made into the default branch are auto-deployed to the `next` pre-release tag on NPM. The result can be installed with `npm install ts-json-schema-generator@next` + - When merging into `next`, please use the `squash and merge` strategy. +- To release a new stable version, open a PR from `next` into `stable` using this [compare link](https://github.com/vega/ts-json-schema-generator/compare/stable...next). + - When merging from `next` into `stable`, please use the `create a merge commit` strategy. diff --git a/factory/parser.ts b/factory/parser.ts index f3e342669..6bc089000 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -79,7 +79,12 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme if (config.jsDoc === "extended") { return new AnnotatedNodeParser( nodeParser, - new ExtendedAnnotationsReader(typeChecker, extraTags, config.markdownDescription), + new ExtendedAnnotationsReader( + typeChecker, + extraTags, + config.markdownDescription, + config.fullDescription, + ), ); } else if (config.jsDoc === "basic") { return new AnnotatedNodeParser(nodeParser, new BasicAnnotationsReader(extraTags)); diff --git a/src/AnnotationsReader/ExtendedAnnotationsReader.ts b/src/AnnotationsReader/ExtendedAnnotationsReader.ts index 9e2a292ff..30f928e64 100644 --- a/src/AnnotationsReader/ExtendedAnnotationsReader.ts +++ b/src/AnnotationsReader/ExtendedAnnotationsReader.ts @@ -2,6 +2,7 @@ import json5 from "json5"; import type ts from "typescript"; import type { Annotations } from "../Type/AnnotatedType.js"; import { symbolAtNode } from "../Utils/symbolAtNode.js"; +import { getFullDescription } from "../Utils/getFullDescription.js"; import { BasicAnnotationsReader } from "./BasicAnnotationsReader.js"; export class ExtendedAnnotationsReader extends BasicAnnotationsReader { @@ -9,6 +10,7 @@ export class ExtendedAnnotationsReader extends BasicAnnotationsReader { private typeChecker: ts.TypeChecker, extraTags?: Set, private markdownDescription?: boolean, + private fullDescription?: boolean, ) { super(extraTags); } @@ -44,21 +46,34 @@ export class ExtendedAnnotationsReader extends BasicAnnotationsReader { return undefined; } + const annotations: { description?: string; markdownDescription?: string; fullDescription?: string } = {}; + const comments: ts.SymbolDisplayPart[] = symbol.getDocumentationComment(this.typeChecker); - if (!comments || !comments.length) { - return undefined; - } - const markdownDescription = comments - .map((comment) => comment.text) - .join(" ") - .replace(/\r/g, "") - .trim(); + if (comments && comments.length) { + const markdownDescription = comments + .map((comment) => comment.text) + .join(" ") + .replace(/\r/g, "") + .trim(); - const description = markdownDescription.replace(/(?<=[^\n])\n(?=[^\n*-])/g, " ").trim(); + annotations.description = markdownDescription.replace(/(?<=[^\n])\n(?=[^\n*-])/g, " ").trim(); + + if (this.markdownDescription) { + annotations.markdownDescription = markdownDescription; + } + } - return this.markdownDescription ? { description, markdownDescription } : { description }; + if (this.fullDescription) { + const fullDescription = getFullDescription(node)?.trim(); + if (fullDescription) { + annotations.fullDescription = fullDescription; + } + } + + return Object.keys(annotations).length ? annotations : undefined; } + private getTypeAnnotation(node: ts.Node): Annotations | undefined { const symbol = symbolAtNode(node); if (!symbol) { diff --git a/src/Config.ts b/src/Config.ts index 24d126c42..1069c25ad 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -8,6 +8,7 @@ export interface Config { topRef?: boolean; jsDoc?: "none" | "extended" | "basic"; markdownDescription?: boolean; + fullDescription?: boolean; sortProps?: boolean; strictTuples?: boolean; skipTypeCheck?: boolean; @@ -27,6 +28,7 @@ export const DEFAULT_CONFIG: Omit, "path" | "type" | "schemaId" topRef: true, jsDoc: "extended", markdownDescription: false, + fullDescription: false, sortProps: true, strictTuples: false, skipTypeCheck: false, diff --git a/src/Utils/getFullDescription.ts b/src/Utils/getFullDescription.ts new file mode 100644 index 000000000..64c7da285 --- /dev/null +++ b/src/Utils/getFullDescription.ts @@ -0,0 +1,39 @@ +import ts from "typescript"; + +export function getFullDescription(node: ts.Node): string | undefined { + const sourceFile = node.getSourceFile(); + const jsDocNodes = ts.getJSDocCommentsAndTags(node); + + if (!jsDocNodes || jsDocNodes.length === 0) { + return undefined; + } + + let rawText = ""; + + for (const jsDoc of jsDocNodes) { + rawText += jsDoc.getFullText(sourceFile) + "\n"; + } + + rawText = rawText.trim(); + + return getTextWithoutStars(rawText).trim(); +} + +function getTextWithoutStars(inputText: string) { + const innerTextWithStars = inputText.replace(/^\/\*\*[^\S\n]*\n?/, "").replace(/(\r?\n)?[^\S\n]*\*\/$/, ""); + + return innerTextWithStars + .split(/\n/) + .map((line) => { + const trimmedLine = line.trimStart(); + + if (trimmedLine[0] !== "*") { + return line; + } + + const textStartPos = trimmedLine[1] === " " ? 2 : 1; + + return trimmedLine.substring(textStartPos); + }) + .join("\n"); +} diff --git a/test/config.test.ts b/test/config.test.ts index 5cd680e01..5386a7569 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -64,10 +64,14 @@ function assertSchema( expect(typeof actual).toBe("object"); expect(actual).toEqual(expected); + const keywords: string[] = []; + if (config.markdownDescription) keywords.push("markdownDescription"); + if (config.fullDescription) keywords.push("fullDescription"); + const validator = new Ajv({ // skip full check if we are not encoding refs validateFormats: config.encodeRefs === false ? undefined : true, - keywords: config.markdownDescription ? ["markdownDescription"] : undefined, + keywords: keywords.length ? keywords : undefined, }); addFormats(validator); @@ -341,6 +345,18 @@ describe("config", () => { markdownDescription: true, }), ); + it( + "full-description", + assertSchema("full-description", { + type: "MyObject", + expose: "export", + topRef: false, + jsDoc: "extended", + sortProps: true, + markdownDescription: true, + fullDescription: true, + }), + ); it( "tsconfig-support", assertSchema( diff --git a/test/config/full-description/main.ts b/test/config/full-description/main.ts new file mode 100644 index 000000000..0629ee94f --- /dev/null +++ b/test/config/full-description/main.ts @@ -0,0 +1,169 @@ +/** + * @title Raw Test Schema Interface + * @description Top-level interface: This interface is used to test the fullDescription output. + * It includes various formatting quirks, inline tags such as {@link SomeReference}, and multiple JSDoc sections. + * + * @markdownDescription **Markdown version:** Markdown description which should be preserved + * + * Additional info: Top-level details should be completely preserved in raw form. + */ +export interface MyObject { + /** + * @title Single-line Title and Description + * @description Single-line comment for raw extraction. + */ + singleLine: string; + + /** + * @title Multiline Field Title + * @description This is a multiline description. + * It spans multiple lines to test the preservation of newlines. + * + * @note 123 + * @format date-time + */ + multilineField: number; + + /** + * This field has a comment without explicit tags. + * It includes an inline reference: {@link ExampleReference} and extra text on several lines. + * + * The raw output should preserve this whole block as is. + */ + noTagField: boolean; + + /** + * @title Field with Special Formatting + * @description Extra spaces and indentation should be preserved. + * + * @pattern /^\w+$/ + */ + specialFormat: string; + + /** + * Some initial descriptive text that is not tagged. + * + * @description Field with initial untagged text followed by a tag. + * @default 42 + * + * Further comments in the same block should be preserved entirely. + */ + initialText: number; + + /** + * Some *code block*: + * ```yaml + * name: description + * length: 42 + * ``` + * + * Some list: + * - one + * - two + * - and three... + * + * @description This field tests `inline code` and bold text. + * + * Also includes an inline link: {@link https://example.com} and additional commentary. + */ + markdownField: string; + + /** + * @title Tag Only Field + * @note Only raw content should be available. + * @customTag Tag only content! + */ + tagOnlyField: string; + + /** + * @title Tag Only Field + * @description + * @note Only raw content should be available. + * @customTag Tag only content! + */ + tagOnlyFieldWithDescription: string; + + /** Some text */ + oneLineJsDoc: string; + + /** Some *text* - {@link https://example.com} - @see the `link` */ + oneLineJsDocComplex: string; + + /** */ + emptyJsDoc1?: null; + + /** + * + */ + emptyJsDoc2?: null; + + noJsDoc?: null; + + /** + * Some ignored comment description + * + * @description Export field description + * @default {"length": 10} + * @nullable + */ + exportString: MyExportString; + /** + * @description Export field description + * @default "private" + */ + privateString: MyPrivateString; + + /** + * @title Non empty array + */ + numberArray: MyNonEmptyArray; + + /** + * @nullable + */ + number: number; + + /** + * Some more examples: + * ```yaml + * name: description + * length: 42 + * ``` + */ + description: InheritedExample["description"]; + + /** + * @default "" + */ + inheritedDescription: InheritedExample["description"]; +} + +/** + * @title My export string + */ +export type MyExportString = string; +/** + * @title My private string + */ +type MyPrivateString = string; +/** + * @minItems 1 + */ +export type MyNonEmptyArray = T[]; + +/** + * @title Inherited Example Interface + * @description This interface is used to test inherited descriptions. + */ +export interface InheritedExample { + /** + * This is an inherited description. + * + * It may include multiple at-tags: + * @title Inherited Title + * @description Inherited description text. + * + * It contains an inline link: {@link https://example.com} and additional commentary. + */ + description: string; +} diff --git a/test/config/full-description/schema.json b/test/config/full-description/schema.json new file mode 100644 index 000000000..c775f1eec --- /dev/null +++ b/test/config/full-description/schema.json @@ -0,0 +1,161 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "MyExportString": { + "fullDescription": "@title My export string", + "title": "My export string", + "type": "string" + }, + "MyNonEmptyArray": { + "items": { + "type": "number" + }, + "minItems": 1, + "fullDescription": "@minItems 1", + "type": "array" + } + }, + "description": "Top-level interface: This interface is used to test the fullDescription output.\nIt includes various formatting quirks, inline tags such as {@link SomeReference }, and multiple JSDoc sections.", + "properties": { + "description": { + "description": "Some more examples: ```yaml name: description length: 42 ```", + "markdownDescription": "Some more examples:\n```yaml\nname: description\nlength: 42\n```", + "fullDescription": "Some more examples:\n```yaml\nname: description\nlength: 42\n```", + "title": "Inherited Title", + "type": "string" + }, + "emptyJsDoc1": { + "type": "null" + }, + "emptyJsDoc2": { + "type": "null" + }, + "exportString": { + "anyOf": [ + { + "$ref": "#/definitions/MyExportString", + "markdownDescription": "Some ignored comment description", + "fullDescription": "Some ignored comment description\n\n@description Export field description\n@default {\"length\": 10}\n@nullable" + }, + { + "type": "null" + } + ], + "default": { + "length": 10 + }, + "description": "Export field description" + }, + "inheritedDescription": { + "default": "", + "description": "Inherited description text.\n\nIt contains an inline link: {@link https://example.com} and additional commentary.", + "markdownDescription": "This is an inherited description.\n\nIt may include multiple at-tags:", + "fullDescription": "@default \"\"", + "title": "Inherited Title", + "type": "string" + }, + "initialText": { + "default": "42\n\nFurther comments in the same block should be preserved entirely.", + "description": "Field with initial untagged text followed by a tag.", + "markdownDescription": "Some initial descriptive text that is not tagged.", + "fullDescription": "Some initial descriptive text that is not tagged.\n\n@description Field with initial untagged text followed by a tag.\n@default 42\n\nFurther comments in the same block should be preserved entirely.", + "type": "number" + }, + "markdownField": { + "description": "This field tests `inline code` and bold text.\n\nAlso includes an inline link: {@link https://example.com} and additional commentary.", + "markdownDescription": "Some *code block*:\n```yaml\nname: description\nlength: 42\n```\n\nSome list:\n- one\n- two\n- and three...", + "fullDescription": "Some *code block*:\n```yaml\nname: description\nlength: 42\n```\n\nSome list:\n- one\n- two\n- and three...\n\n@description This field tests `inline code` and bold text.\n\nAlso includes an inline link: {@link https://example.com} and additional commentary.", + "type": "string" + }, + "multilineField": { + "description": "This is a multiline description.\nIt spans multiple lines to test the preservation of newlines.", + "format": "date-time", + "fullDescription": "@title Multiline Field Title\n@description This is a multiline description.\nIt spans multiple lines to test the preservation of newlines.\n\n@note 123\n@format date-time", + "title": "Multiline Field Title", + "type": "number" + }, + "noJsDoc": { + "type": "null" + }, + "noTagField": { + "description": "This field has a comment without explicit tags. It includes an inline reference: {@link ExampleReference } and extra text on several lines.\n\nThe raw output should preserve this whole block as is.", + "markdownDescription": "This field has a comment without explicit tags.\nIt includes an inline reference: {@link ExampleReference } and extra text on several lines.\n\nThe raw output should preserve this whole block as is.", + "fullDescription": "This field has a comment without explicit tags.\nIt includes an inline reference: {@link ExampleReference} and extra text on several lines.\n\nThe raw output should preserve this whole block as is.", + "type": "boolean" + }, + "number": { + "fullDescription": "@nullable", + "type": ["number", "null"] + }, + "numberArray": { + "$ref": "#/definitions/MyNonEmptyArray%3Cnumber%3E", + "fullDescription": "@title Non empty array", + "title": "Non empty array" + }, + "oneLineJsDoc": { + "description": "Some text", + "markdownDescription": "Some text", + "fullDescription": "Some text", + "type": "string" + }, + "oneLineJsDocComplex": { + "description": "Some *text* - {@link https://example.com } -", + "markdownDescription": "Some *text* - {@link https://example.com } -", + "fullDescription": "Some *text* - {@link https://example.com} - @see the `link`", + "type": "string" + }, + "privateString": { + "default": "private", + "description": "Export field description", + "fullDescription": "@description Export field description\n@default \"private\"", + "title": "My private string", + "type": "string" + }, + "singleLine": { + "description": "Single-line comment for raw extraction.", + "fullDescription": "@title Single-line Title and Description\n@description Single-line comment for raw extraction.", + "title": "Single-line Title and Description", + "type": "string" + }, + "specialFormat": { + "description": "Extra spaces and indentation should be preserved.", + "pattern": "/^\\w+$/", + "fullDescription": "@title Field with Special Formatting\n@description Extra spaces and indentation should be preserved.\n\n@pattern /^\\w+$/", + "title": "Field with Special Formatting", + "type": "string" + }, + "tagOnlyField": { + "fullDescription": "@title Tag Only Field\n@note Only raw content should be available.\n@customTag Tag only content!", + "title": "Tag Only Field", + "type": "string" + }, + "tagOnlyFieldWithDescription": { + "description": "", + "fullDescription": "@title Tag Only Field\n@description\n@note Only raw content should be available.\n@customTag Tag only content!", + "title": "Tag Only Field", + "type": "string" + } + }, + "fullDescription": "@title Raw Test Schema Interface\n@description Top-level interface: This interface is used to test the fullDescription output.\nIt includes various formatting quirks, inline tags such as {@link SomeReference}, and multiple JSDoc sections.\n\n@markdownDescription **Markdown version:** Markdown description which should be preserved\n\nAdditional info: Top-level details should be completely preserved in raw form.", + "required": [ + "singleLine", + "multilineField", + "noTagField", + "specialFormat", + "initialText", + "markdownField", + "tagOnlyField", + "tagOnlyFieldWithDescription", + "oneLineJsDoc", + "oneLineJsDocComplex", + "exportString", + "privateString", + "numberArray", + "number", + "description", + "inheritedDescription" + ], + "title": "Raw Test Schema Interface", + "type": "object" +} diff --git a/test/unit/getFullDescription.test.ts b/test/unit/getFullDescription.test.ts new file mode 100644 index 000000000..c39cfc2ae --- /dev/null +++ b/test/unit/getFullDescription.test.ts @@ -0,0 +1,236 @@ +import { getFullDescription } from "../../src/Utils/getFullDescription"; +import ts from "typescript"; + +function dummyNode(jsDocComment: string): ts.Node { + const code = `${jsDocComment}\nfunction dummy() {}`; + const sourceFile = ts.createSourceFile("dummy.ts", code, ts.ScriptTarget.Latest, true); + const node = sourceFile.statements.find(ts.isFunctionDeclaration); + if (!node) { + throw new Error("Could not create node"); + } + return node; +} + +// All white-space charachters except new-line, starting and ending with simple space +const ANY_SPACE = " \t\f\v\r\u00A0\u2028\u2029 "; + +describe("getFullDescription", () => { + it("Returns undefined if no JSDoc", () => { + const jsdoc = "// no JSDoc"; + const result = getFullDescription(dummyNode(jsdoc)); + expect(result).toBeUndefined(); + }); + + const cases = [ + { + desc: "Removes single-space padding from a one-line JSDoc", + jsdoc: "/** Some text */", + expected: "Some text", + }, + { + desc: "Removes multiple-space padding from a one-line JSDoc", + jsdoc: `/**${ANY_SPACE}Some text${ANY_SPACE}*/`, + expected: "Some text", + }, + { + desc: "Extracts text between JSDoc markers when no padding is present", + jsdoc: "/**Some text*/", + expected: "Some text", + }, + { + desc: "Removes extra whitespace from a multi-line JSDoc with a single line of text", + jsdoc: ` + /** + * Some text + */`, + expected: "Some text", + }, + { + desc: "Removes irregular whitespace from a multi-line JSDoc on a single text line", + jsdoc: ` + /** + * ${ANY_SPACE}Some text${ANY_SPACE} + */`, + expected: "Some text", + }, + { + desc: "Extracts trimmed text from a multi-line JSDoc with a single non-empty line", + jsdoc: ` + /** ${ANY_SPACE} + * ${ANY_SPACE} + * ${ANY_SPACE}Some text${ANY_SPACE} + * ${ANY_SPACE} + ${ANY_SPACE} */ + `, + expected: "Some text", + }, + { + desc: "Processes multi-line JSDoc where some lines lack a space after the asterisk", + jsdoc: ` + /** + * Line 1 + *Line 2 + */`, + expected: "Line 1\nLine 2", + }, + { + desc: "Preserves intentional leading and trailing spaces within text lines in a multi-line JSDoc", + jsdoc: ` + /** + * Line 1 ${ANY_SPACE} + * ${ANY_SPACE}Line 2 + * ${ANY_SPACE} + * Line 4 + */`, + expected: `Line 1 ${ANY_SPACE}\n${ANY_SPACE}Line 2\n${ANY_SPACE}\nLine 4`, + }, + { + desc: "Preserves at-tags in a one-line JSDoc", + jsdoc: ` + /** @see https://example.com */`, + expected: `@see https://example.com`, + }, + { + desc: "Preserves inline at-tags in a one-line JSDoc", + jsdoc: ` + /** {@link https://example.com} */`, + expected: `{@link https://example.com}`, + }, + { + desc: "Preserves at-tags in a multi-line JSDoc", + jsdoc: ` + /** + * Some text + * @see https://example.com + * More text + */`, + expected: `Some text\n@see https://example.com\nMore text`, + }, + { + desc: "Preserves inline at-tags within a multi-line JSDoc", + jsdoc: ` + /** + * Some text + * Link {@link https://example.com} + * More text + */`, + expected: `Some text\nLink {@link https://example.com}\nMore text`, + }, + { + desc: "Handles multi-line JSDoc comments with asterisks and indentation", + jsdoc: ` + /** + * Some list: + * * Item A + * * Item B + * * Nested Item + * More text + */`, + expected: `Some list:\n* Item A\n* Item B\n * Nested Item\nMore text`, + }, + { + desc: "Preserves Unicode characters", + jsdoc: ` + /** + * \u2013\u2014\u2026\uD83D\uDE0A + */`, + expected: `\u2013\u2014\u2026\uD83D\uDE0A`, + }, + { + desc: "Handles case when end marker is on a new line", + jsdoc: ` + /** Some text + */`, + expected: "Some text", + }, + { + desc: "Handles case when the content and end marker on the other line than start marker", + jsdoc: ` + /** + Some text */`, + expected: "Some text", + }, + { + desc: "Handles case when there is no new line or space after start marker", + jsdoc: ` + /**Some text + */`, + expected: "Some text", + }, + { + desc: "Handles case when there is no new line or space before end marker", + jsdoc: ` + /** + * Some text*/`, + expected: "Some text", + }, + { + desc: "Preserves CRLF in multi-line JSDoc", + jsdoc: `/**\r\n * Line 1\r\nLine 2\r\n */`, + expected: "Line 1\r\nLine 2", + }, + { + desc: "Returns an empty string for an empty JSDoc", + jsdoc: "/***/", + expected: "", + }, + { + desc: "Returns an empty string for a one-line JSDoc containing only whitespace", + jsdoc: "/** */", + expected: "", + }, + { + desc: "Returns an empty string for a JSDoc with only newline characters", + jsdoc: `/**\n\n*/`, + expected: "", + }, + { + desc: "Returns an empty string for a multi-line JSDoc where all lines are empty or whitespace-only", + jsdoc: ` + /** + * + * ${ANY_SPACE} + * + */ + `, + expected: "", + }, + { + desc: "Returns an empty string for a one-line JSDoc containing only whitespace", + jsdoc: `/** ${ANY_SPACE} */`, + expected: "", + }, + + { + desc: `Handles multi-line JSDoc where the first content lines do not begin with an asterisk`, + jsdoc: `/**\nLine 1\nLine 2 + * Line 3 + */`, + expected: "Line 1\nLine 2\nLine 3", + }, + { + desc: `Handles multi-line JSDoc where a middle line lacks an asterisk prefix`, + jsdoc: ` + /** + * Line 1\nLine 2 + * Line 3 + */`, + expected: "Line 1\nLine 2\nLine 3", + }, + { + desc: `Handles non-standard whitespace preceding asterisks in a multi-line JSDoc`, + jsdoc: ` + /** + ${ANY_SPACE}* + ${ANY_SPACE}* Some text + ${ANY_SPACE}* + */`, + expected: "Some text", + }, + ]; + + it.each(cases)("$desc", ({ jsdoc, expected }) => { + const result = getFullDescription(dummyNode(jsdoc.trim())); + expect(result).toBe(expected); + }); +}); diff --git a/ts-json-schema-generator.ts b/ts-json-schema-generator.ts index 29b0c452c..0ed6a2a02 100644 --- a/ts-json-schema-generator.ts +++ b/ts-json-schema-generator.ts @@ -27,6 +27,14 @@ const args = new Command() jsDoc: "extended", }), ) + .addOption( + new Option( + "--full-description", + "Include the full raw JSDoc comment as `fullDescription` in the schema.", + ).implies({ + jsDoc: "extended", + }), + ) .addOption( new Option( "--functions ", @@ -65,6 +73,7 @@ const config: Config = { topRef: args.topRef, jsDoc: args.jsDoc, markdownDescription: args.markdownDescription, + fullDescription: args.fullDescription, sortProps: !args.unstable, strictTuples: args.strictTuples, skipTypeCheck: !args.typeCheck,