From 9454b68c3da7036ff5d1c0f527589c0dda4de090 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:18:26 +0000 Subject: [PATCH 1/4] feat: Don't strip line breaks when parsing TSDoc comments. --- common/config/rush/command-line.json | 18 +++----- .../DocNodeTransforms.test.ts.snap | 39 ++++++++++++++++- tsdoc/src/emitters/TSDocEmitter.ts | 4 ++ .../emitters/__tests__/TSDocEmitter.test.ts | 5 +++ tsdoc/src/transforms/TrimSpacesTransform.ts | 43 +++++++++++-------- 5 files changed, 79 insertions(+), 30 deletions(-) diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index 710f5ddf..72eca02e 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -4,7 +4,6 @@ */ { "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", - /** * Custom "commands" introduce new verbs for the command-line. To see the help for these * example commands, try "rush --help", "rush my-bulk-command --help", or @@ -16,7 +15,6 @@ "name": "build", "phases": ["_phase:build"] }, - { "commandKind": "phased", "name": "test", @@ -25,7 +23,6 @@ "enableParallelism": true, "incremental": true }, - { "commandKind": "phased", "name": "retest", @@ -34,14 +31,12 @@ "enableParallelism": true, "incremental": false }, - { "commandKind": "phased", "name": "start", "summary": "Build all projects, then watch for changes, build and test.", "description": "Build all projects, then watches for changes and builds and runs tests for affected projects.", "safeForSimultaneousRushProcesses": false, - "enableParallelism": true, "incremental": true, // Initial execution only uses the build phase so that all dependencies of watch phases have been built @@ -217,21 +212,16 @@ // */ // // "autoinstallerName": "my-task" // } - { "name": "prettier", "commandKind": "global", "summary": "Used by the pre-commit Git hook. This command invokes Prettier to reformat staged changes.", - "autoinstallerName": "rush-prettier", - // This will invoke common/autoinstall/rush-prettier/node_modules/.bin/pretty-quick "shellCommand": "pretty-quick --staged", - "safeForSimultaneousRushProcesses": true } ], - "phases": [ { "name": "_phase:build", @@ -250,7 +240,6 @@ "allowWarningsOnSuccess": false } ], - /** * Custom "parameters" introduce new parameters for specified Rush command-line commands. * For example, you might define a "--production" parameter for the "rush build" command. @@ -475,6 +464,13 @@ "description": "Perform a production build, including minification optimizations", "associatedPhases": ["_phase:build", "_phase:test"], "associatedCommands": ["build", "rebuild", "test", "retest"] + }, + { + "longName": "--update-snapshots", + "parameterKind": "flag", + "description": "Update unit test snapshots for all projects", + "associatedCommands": ["test", "retest"], + "associatedPhases": ["_phase:test"] } ] } diff --git a/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap b/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap index a6a4f272..1bab7774 100644 --- a/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap +++ b/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap @@ -6,7 +6,21 @@ Object { "nodes": Array [ Object { "kind": "PlainText", - "nodePlainText": "This is the first ", + "nodePlainText": "", + }, + Object { + "kind": "SoftBreak", + }, + Object { + "kind": "PlainText", + "nodePlainText": "This is the", + }, + Object { + "kind": "SoftBreak", + }, + Object { + "kind": "PlainText", + "nodePlainText": " first ", }, Object { "kind": "InlineTag", @@ -27,7 +41,28 @@ Object { }, Object { "kind": "PlainText", - "nodePlainText": "sentence. This is another sentence.", + "nodePlainText": "sentence.", + }, + Object { + "kind": "SoftBreak", + }, + Object { + "kind": "PlainText", + "nodePlainText": " This is another", + }, + Object { + "kind": "SoftBreak", + }, + Object { + "kind": "PlainText", + "nodePlainText": " sentence.", + }, + Object { + "kind": "SoftBreak", + }, + Object { + "kind": "PlainText", + "nodePlainText": "", }, ], } diff --git a/tsdoc/src/emitters/TSDocEmitter.ts b/tsdoc/src/emitters/TSDocEmitter.ts index 1d2d5e6f..d812e4d1 100644 --- a/tsdoc/src/emitters/TSDocEmitter.ts +++ b/tsdoc/src/emitters/TSDocEmitter.ts @@ -313,6 +313,10 @@ export class TSDocEmitter { this._renderNodes(docSection.nodes); break; + case DocNodeKind.SoftBreak: + this._ensureAtStartOfLine(); + break; + case DocNodeKind.Paragraph: const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph( docNode as DocParagraph diff --git a/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts b/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts index 055e9315..101ffee4 100644 --- a/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts +++ b/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts @@ -37,6 +37,7 @@ Object { "output": " /** * x + * */ ", } @@ -47,6 +48,7 @@ Object { "output": " /** * x + * */ ", } @@ -63,6 +65,7 @@ Object { "output": " /** * x + * */ ", } @@ -110,6 +113,8 @@ Object { * blah * * @example + * + * * \`\`\`ts * line1 * line2 diff --git a/tsdoc/src/transforms/TrimSpacesTransform.ts b/tsdoc/src/transforms/TrimSpacesTransform.ts index fbe660f4..e524fc4a 100644 --- a/tsdoc/src/transforms/TrimSpacesTransform.ts +++ b/tsdoc/src/transforms/TrimSpacesTransform.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { DocParagraph, type DocNode, DocNodeKind, DocPlainText } from '../nodes'; +import { DocParagraph, type DocNode, DocNodeKind, DocPlainText, DocSoftBreak } from '../nodes'; /** * Implementation of DocNodeTransforms.trimSpacesInParagraphNodes() @@ -21,6 +21,28 @@ export class TrimSpacesTransform { // as soon as nonempty content is encountered. let finishedSkippingLeadingSpaces: boolean = false; + function pushAccumulatedText(): void { + const lines: string[] = accumulatedTextChunks.join('').split('\n'); + for (let i: number = 0; i < lines.length; i++) { + const line: string = lines[i]; + transformedNodes.push( + new DocPlainText({ + configuration: docParagraph.configuration, + text: line + }) + ); + if (i < lines.length - 1) { + transformedNodes.push( + new DocSoftBreak({ + configuration: docParagraph.configuration + }) + ); + } + } + accumulatedTextChunks.length = 0; + accumulatedNodes.length = 0; + } + for (const node of docParagraph.nodes) { switch (node.kind) { case DocNodeKind.PlainText: @@ -56,6 +78,7 @@ export class TrimSpacesTransform { if (finishedSkippingLeadingSpaces) { pendingSpace = true; } + accumulatedTextChunks.push('\n'); accumulatedNodes.push(node); break; default: @@ -68,14 +91,7 @@ export class TrimSpacesTransform { if (accumulatedTextChunks.length > 0) { // TODO: We should probably track the accumulatedNodes somehow, e.g. so we can map them back to the // original excerpts. But we need a developer scenario before we can design this API. - transformedNodes.push( - new DocPlainText({ - configuration: docParagraph.configuration, - text: accumulatedTextChunks.join('') - }) - ); - accumulatedTextChunks.length = 0; - accumulatedNodes.length = 0; + pushAccumulatedText(); } transformedNodes.push(node); @@ -85,14 +101,7 @@ export class TrimSpacesTransform { // Push the accumulated text if (accumulatedTextChunks.length > 0) { - transformedNodes.push( - new DocPlainText({ - configuration: docParagraph.configuration, - text: accumulatedTextChunks.join('') - }) - ); - accumulatedTextChunks.length = 0; - accumulatedNodes.length = 0; + pushAccumulatedText(); } const transformedParagraph: DocParagraph = new DocParagraph({ From d40d44c28476f3c63d7a4a6b2284a94317067bf6 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:19:44 +0000 Subject: [PATCH 2/4] docs: Add change --- ...smithr-dont-strip-line-breaks_2025-10-07-21-19.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json diff --git a/common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json b/common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json new file mode 100644 index 00000000..a939eb33 --- /dev/null +++ b/common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/tsdoc", + "comment": "Update parser to not replace soft line breaks in parsed comments", + "type": "major" + } + ], + "packageName": "@microsoft/tsdoc" +} \ No newline at end of file From cd442cb7e2759355b7196b3a6ca56f8db6f49e20 Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:29:33 +0000 Subject: [PATCH 3/4] docs: Update changeset --- .../tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json b/common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json index a939eb33..f069e8fa 100644 --- a/common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json +++ b/common/changes/@microsoft/tsdoc/josmithr-dont-strip-line-breaks_2025-10-07-21-19.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/tsdoc", - "comment": "Update parser to not replace soft line breaks in parsed comments", + "comment": "Update emitter to not replace line breaks", "type": "major" } ], From d64352b72c80d15508c3284e47802c28318805fc Mon Sep 17 00:00:00 2001 From: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:43:18 +0000 Subject: [PATCH 4/4] fix: Don't emit blank lines within a paragraph --- .../DocNodeTransforms.test.ts.snap | 15 ++---------- .../emitters/__tests__/TSDocEmitter.test.ts | 5 ---- tsdoc/src/transforms/DocNodeTransforms.ts | 9 ++++---- tsdoc/src/transforms/TrimSpacesTransform.ts | 23 +++++++++---------- 4 files changed, 17 insertions(+), 35 deletions(-) diff --git a/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap b/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap index 1bab7774..41655726 100644 --- a/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap +++ b/tsdoc/src/__tests__/__snapshots__/DocNodeTransforms.test.ts.snap @@ -4,10 +4,6 @@ exports[`01 trimSpacesInParagraphNodes() 1`] = ` Object { "kind": "Paragraph", "nodes": Array [ - Object { - "kind": "PlainText", - "nodePlainText": "", - }, Object { "kind": "SoftBreak", }, @@ -20,7 +16,7 @@ Object { }, Object { "kind": "PlainText", - "nodePlainText": " first ", + "nodePlainText": "first ", }, Object { "kind": "InlineTag", @@ -55,14 +51,7 @@ Object { }, Object { "kind": "PlainText", - "nodePlainText": " sentence.", - }, - Object { - "kind": "SoftBreak", - }, - Object { - "kind": "PlainText", - "nodePlainText": "", + "nodePlainText": "sentence.", }, ], } diff --git a/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts b/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts index 101ffee4..055e9315 100644 --- a/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts +++ b/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts @@ -37,7 +37,6 @@ Object { "output": " /** * x - * */ ", } @@ -48,7 +47,6 @@ Object { "output": " /** * x - * */ ", } @@ -65,7 +63,6 @@ Object { "output": " /** * x - * */ ", } @@ -113,8 +110,6 @@ Object { * blah * * @example - * - * * \`\`\`ts * line1 * line2 diff --git a/tsdoc/src/transforms/DocNodeTransforms.ts b/tsdoc/src/transforms/DocNodeTransforms.ts index 0ae9fe60..1202973d 100644 --- a/tsdoc/src/transforms/DocNodeTransforms.ts +++ b/tsdoc/src/transforms/DocNodeTransforms.ts @@ -35,16 +35,15 @@ export class DocNodeTransforms { * * ``` * nodes: [ - * { kind: PlainText, text: "Here are some " }, - * { kind: PlainText, text: "words " }, + * { kind: PlainText, text: "Here are some" }, + * { kind: SoftBreak } + * { kind: PlainText, text: "words" }, + * { kind: SoftBreak } * { kind: InlineTag, text: "{\@inheritDoc}" }, * { kind: PlainText, text: "to process." } * ] * ``` * - * Note that in this example, `"words "` is not merged with the preceding node because - * its DocPlainText.excerpt cannot span multiple lines. - * * @param docParagraph - a DocParagraph containing nodes to be transformed * @returns The transformed child nodes. */ diff --git a/tsdoc/src/transforms/TrimSpacesTransform.ts b/tsdoc/src/transforms/TrimSpacesTransform.ts index e524fc4a..5f0dcdea 100644 --- a/tsdoc/src/transforms/TrimSpacesTransform.ts +++ b/tsdoc/src/transforms/TrimSpacesTransform.ts @@ -25,16 +25,18 @@ export class TrimSpacesTransform { const lines: string[] = accumulatedTextChunks.join('').split('\n'); for (let i: number = 0; i < lines.length; i++) { const line: string = lines[i]; - transformedNodes.push( - new DocPlainText({ - configuration: docParagraph.configuration, - text: line - }) - ); - if (i < lines.length - 1) { + if (line.length !== 0) { + if (i !== 0) { + transformedNodes.push( + new DocSoftBreak({ + configuration: docParagraph.configuration + }) + ); + } transformedNodes.push( - new DocSoftBreak({ - configuration: docParagraph.configuration + new DocPlainText({ + configuration: docParagraph.configuration, + text: line }) ); } @@ -75,9 +77,6 @@ export class TrimSpacesTransform { } break; case DocNodeKind.SoftBreak: - if (finishedSkippingLeadingSpaces) { - pendingSpace = true; - } accumulatedTextChunks.push('\n'); accumulatedNodes.push(node); break;