From a763b5b14289325f419c7f4284c9214255fdd4dd Mon Sep 17 00:00:00 2001 From: "C. Spencer Beggs" <109114+spencerbeggs@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:53:15 -0400 Subject: [PATCH] fix(tsdoc): preserve soft break before inline nodes in TSDocEmitter Since 0.17.0, TrimSpacesTransform dropped a soft break that immediately preceded an inline node such as a code span, {@link} tag, or HTML element. pushAccumulatedText() only emitted a DocSoftBreak ahead of a nonempty line, so text ending in a line break lost it and was joined to the following node (e.g. "policy is`fail`"). Emit the trailing soft break when the accumulated text is flushed ahead of an inline node. The trailing break at the end of a paragraph is still discarded, so the output of trimSpacesInParagraph() is otherwise unchanged. Fixes #493 Signed-off-by: C. Spencer Beggs --- .../tsdoc/fix-issue-493_2026-09-27-18-52.json | 11 ++++ .../emitters/__tests__/TSDocEmitter.test.ts | 51 +++++++++++++++++++ tsdoc/src/transforms/TrimSpacesTransform.ts | 18 +++++-- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 common/changes/@microsoft/tsdoc/fix-issue-493_2026-09-27-18-52.json diff --git a/common/changes/@microsoft/tsdoc/fix-issue-493_2026-09-27-18-52.json b/common/changes/@microsoft/tsdoc/fix-issue-493_2026-09-27-18-52.json new file mode 100644 index 00000000..494bceef --- /dev/null +++ b/common/changes/@microsoft/tsdoc/fix-issue-493_2026-09-27-18-52.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/tsdoc", + "comment": "Fix an issue where `TSDocEmitter` dropped a line break that appeared before an inline node such as a code span, `{@link}` tag, or HTML element, joining the adjacent words.", + "type": "patch" + } + ], + "packageName": "@microsoft/tsdoc", + "email": "109114+spencerbeggs@users.noreply.github.com" +} \ No newline at end of file diff --git a/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts b/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts index 055e9315..69fce8ab 100644 --- a/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts +++ b/tsdoc/src/emitters/__tests__/TSDocEmitter.test.ts @@ -150,3 +150,54 @@ test('04 TSDocEmitter.renderDeclarationReference()', () => { }); expect(htmlTag.emitAsTsdoc()).toMatchInlineSnapshot(`"my-package#(MyClass:class)"`); }); + +test('05 Emit soft breaks before inline nodes', () => { + const input: string = ` +/** + * The policy is + * \`fail\`. + * + * See the + * {@link Foo} thing. + * + * This is an + * element. + * + * The policy is + * fail now. + * + * Then \`x\` + * then b. + * + * See {@link Foo} + * \`x\` too. + */ +`; + + expect(createSnapshot(input)).toMatchInlineSnapshot(` +Object { + "errors": Array [], + "output": " +/** + * The policy is + * \`fail\`. + * + * See the + * {@link Foo} thing. + * + * This is an + * element. + * + * The policy is + * fail now. + * + * Then \`x\` + * then b. + * + * See {@link Foo} + * \`x\` too. + */ +", +} +`); +}); diff --git a/tsdoc/src/transforms/TrimSpacesTransform.ts b/tsdoc/src/transforms/TrimSpacesTransform.ts index 5f0dcdea..9571fb38 100644 --- a/tsdoc/src/transforms/TrimSpacesTransform.ts +++ b/tsdoc/src/transforms/TrimSpacesTransform.ts @@ -21,11 +21,21 @@ export class TrimSpacesTransform { // as soon as nonempty content is encountered. let finishedSkippingLeadingSpaces: boolean = false; - function pushAccumulatedText(): void { + function pushAccumulatedText(followedByInlineNode: boolean): void { const lines: string[] = accumulatedTextChunks.join('').split('\n'); for (let i: number = 0; i < lines.length; i++) { const line: string = lines[i]; - if (line.length !== 0) { + if (line.length === 0) { + // If the text ended with a line break and an inline node (e.g. a code span or {@link}) comes next, + // preserve the soft break; otherwise the text and the node would be joined without any separator. + if (followedByInlineNode && i !== 0 && i === lines.length - 1) { + transformedNodes.push( + new DocSoftBreak({ + configuration: docParagraph.configuration + }) + ); + } + } else { if (i !== 0) { transformedNodes.push( new DocSoftBreak({ @@ -90,7 +100,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. - pushAccumulatedText(); + pushAccumulatedText(true); } transformedNodes.push(node); @@ -100,7 +110,7 @@ export class TrimSpacesTransform { // Push the accumulated text if (accumulatedTextChunks.length > 0) { - pushAccumulatedText(); + pushAccumulatedText(false); } const transformedParagraph: DocParagraph = new DocParagraph({