diff --git a/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.spec.ts b/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.spec.ts index 8e46d0a48..0e11e7469 100644 --- a/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.spec.ts +++ b/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.spec.ts @@ -1,22 +1,16 @@ import { formatTableColumn } from './format-table-column'; describe('formatTableColumn', () => { - it('should format table column correctly', () => { - const input = `This is a string with -a newline, | a pipe, and a code block: -\`\`\`ts -const x = 10; -\`\`\``; - const expectedOutput = - 'This is a string with
a newline, \\| a pipe, and a code block:
`const x = 10;`'; - const result = formatTableColumn(input); - expect(result).toEqual(expectedOutput); + it('should correctly escape pipes', () => { + const input = 'This is a test | with a pipe.'; + const expectedOutput = 'This is a test \\| with a pipe.'; + expect(formatTableColumn(input)).toBe(expectedOutput); }); - it('should remove trailing
tags', () => { - const input = 'This is a string with a trailing
tag
'; - const expectedOutput = 'This is a string with a trailing
tag'; - const result = formatTableColumn(input); - expect(result).toEqual(expectedOutput); + it('should correctly convert multi-line markdown to HTML', () => { + const input = `1. First item +2. Second item`; + const expectedOutput = '
  1. First item
  2. Second item
'; + expect(formatTableColumn(input)).toBe(expectedOutput); }); }); diff --git a/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.ts b/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.ts index 4977e0fe3..47b966abf 100644 --- a/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.ts +++ b/packages/typedoc-plugin-markdown/src/libs/utils/format-table-column.ts @@ -1,9 +1,13 @@ +import { markdownBlocksToHtml } from './markdown-blocks-to-html'; +import { normalizeLineBreaks } from './normalize-line-breaks'; + export function formatTableColumn(str: string) { - return str - .replace(/\|/g, '\\|') - .replace(/\n(?=(?:[^`]*`[^`]*`)*[^`]*$)/gi, '
') - .replace(/\`\`\`ts/g, '`') - .replace(/\`\`\`/g, '`') - .replace(/\n/g, '') - .replace(/(
\s*)+$/g, ''); + // Normalize line breaks + let md = normalizeLineBreaks(str); + // If comments are on multiple lines convert markdown block tags to HTML and remove new lines. + if (md.split('\n').length > 1) { + md = markdownBlocksToHtml(md); + } + // Finally return with escaped pipes + return md.replace(/\|/g, '\\|'); } diff --git a/packages/typedoc-plugin-markdown/src/libs/utils/index.ts b/packages/typedoc-plugin-markdown/src/libs/utils/index.ts index dfdc29b4c..52eeeb97b 100644 --- a/packages/typedoc-plugin-markdown/src/libs/utils/index.ts +++ b/packages/typedoc-plugin-markdown/src/libs/utils/index.ts @@ -8,6 +8,8 @@ export { formatMarkdown } from './format-markdown'; export { formatTableColumn } from './format-table-column'; export { getFileNameWithExtension } from './get-file-name-with-extension'; export { isQuoted } from './is-quoted'; +export { markdownBlocksToHtml } from './markdown-blocks-to-html'; +export { normalizeLineBreaks } from './normalize-line-breaks'; export { removeFirstScopedDirectory } from './remove-first-scoped-directory'; export { removeLineBreaks } from './remove-line-breaks'; export { sanitizeComments } from './sanitize-comments'; diff --git a/packages/typedoc-plugin-markdown/src/libs/utils/markdown-blocks-to-html.spec.ts b/packages/typedoc-plugin-markdown/src/libs/utils/markdown-blocks-to-html.spec.ts new file mode 100644 index 000000000..df1a1651b --- /dev/null +++ b/packages/typedoc-plugin-markdown/src/libs/utils/markdown-blocks-to-html.spec.ts @@ -0,0 +1,35 @@ +import { markdownBlocksToHtml } from './markdown-blocks-to-html'; + +describe('markdownBlocksToHtml', () => { + it('should correctly convert markdown to HTML', () => { + const input = `This is a test + +Double new line + +### Heading + +

Subheading

+ +- list item 1 +- list item 2`; + + const expectedOutput = `

This is a test

Double new line

Heading

Subheading

`; + + expect(markdownBlocksToHtml(input)).toBe(expectedOutput); + }); + + it('should correctly convert markdown to HTML', () => { + const input = `

paragraph

+ +New line + +

paragraph

+

+ paragraph with new line +

`; + + const expectedOutput = `

paragraph

New line

paragraph

paragraph with new line

`; + + expect(markdownBlocksToHtml(input)).toBe(expectedOutput); + }); +}); diff --git a/packages/typedoc-plugin-markdown/src/libs/utils/markdown-blocks-to-html.ts b/packages/typedoc-plugin-markdown/src/libs/utils/markdown-blocks-to-html.ts new file mode 100644 index 000000000..8a5309e7c --- /dev/null +++ b/packages/typedoc-plugin-markdown/src/libs/utils/markdown-blocks-to-html.ts @@ -0,0 +1,73 @@ +export function markdownBlocksToHtml(markdownText: string) { + // Remove new lines inside

tags + markdownText = markdownText.replace(/

([\s\S]*?)<\/p>/gm, (match, p1) => { + const contentWithoutNewLinesOrLeadingSpaces = p1 + .replace(/\r?\n|\r/g, ' ') + .replace(/^\s+/, ''); + return `

${contentWithoutNewLinesOrLeadingSpaces}

`; + }); + + // Replace headers + markdownText = markdownText.replace( + /^(#{1,6})\s*(.*?)\s*$/gm, + (match, p1, p2) => { + const level = p1.length; + return `${p2}`; + }, + ); + + // Replace triple code blocks with code + markdownText = markdownText.replace( + /```.*?\n([\s\S]*?)```/gs, + '$1', + ); + + // Replace horizontal rules + markdownText = markdownText.replace(/^[-*_]{3,}\s*$/gm, '
'); + + // Replace unordered lists + markdownText = markdownText.replace(/^(\s*-\s+.+$(\r?\n)?)+/gm, (match) => { + const items = match.trim().split('\n'); + const listItems = items + .map((item) => `
  • ${item.trim().substring(2)}
  • `) + .join(''); + return ``; + }); + + // Replace ordered lists + markdownText = markdownText.replace( + /^(\s*\d+\.\s+.+$(\r?\n)?)+/gm, + (match) => { + const items = match.trim().split('\n'); + const listItems = items + .map( + (item) => `
  • ${item.trim().substring(item.indexOf('.') + 2)}
  • `, + ) + .join(''); + return `
      ${listItems}
    `; + }, + ); + + // Replace paragraphs + markdownText = markdownText.replace( + /^(?!.*<[^>]+>)(.+?)(?:(?:\r\n|\r|\n){2,}|$)(?!.*<[^>]+>)/gm, + '

    $1

    ', + ); + + // Replace ordered lists + markdownText = markdownText.replace( + /^(\s*\d+\.\s+.+$(\r?\n)?)+/gm, + (match) => { + const items = match.trim().split('\n'); + const listItems = items + .map( + (item) => `
  • ${item.trim().substring(item.indexOf('.') + 1)}
  • `, + ) + .join(''); + return `
      ${listItems}
    `; + }, + ); + + // Finally remove all new lines + return markdownText.replace(/\n/g, ''); +} diff --git a/packages/typedoc-plugin-markdown/src/libs/utils/normalize-line-breaks.spec.ts b/packages/typedoc-plugin-markdown/src/libs/utils/normalize-line-breaks.spec.ts new file mode 100644 index 000000000..4122cfad7 --- /dev/null +++ b/packages/typedoc-plugin-markdown/src/libs/utils/normalize-line-breaks.spec.ts @@ -0,0 +1,24 @@ +import { normalizeLineBreaks } from './normalize-line-breaks'; + +describe('normalizeLineBreaks', () => { + it('should correctly concatenate lines', () => { + const input = `This line should be concatenated with the next one. +The next line. + +This is the next line double break. +- list item 1 +- list item 2 + +This is another test.`; + + const expectedOutput = `This line should be concatenated with the next one. The next line. + +This is the next line double break. +- list item 1 +- list item 2 + +This is another test.`; + + expect(normalizeLineBreaks(input)).toBe(expectedOutput); + }); +}); diff --git a/packages/typedoc-plugin-markdown/src/libs/utils/normalize-line-breaks.ts b/packages/typedoc-plugin-markdown/src/libs/utils/normalize-line-breaks.ts new file mode 100644 index 000000000..cb4a533af --- /dev/null +++ b/packages/typedoc-plugin-markdown/src/libs/utils/normalize-line-breaks.ts @@ -0,0 +1,38 @@ +export function normalizeLineBreaks(str: string): string { + const codeBlocks: string[] = []; + + const placeholder = '\n___CODEBLOCKPLACEHOLDER___\n'; + str = str.replace(/```[\s\S]*?```/g, (match) => { + codeBlocks.push(match); + return placeholder; + }); + + const lines = str.split('\n'); + let result = ''; + for (let i = 0; i < lines.length; i++) { + if (lines[i].length === 0) { + result = result + lines[i] + '\n'; + } else { + if ( + !lines[i].startsWith('#') && + lines[i + 1] && + /^[a-zA-Z`]/.test(lines[i + 1]) + ) { + result = result + lines[i] + ' '; + } else { + if (i < lines.length - 1) { + result = result + lines[i] + '\n'; + } else { + result = result + lines[i]; + } + } + } + } + + result = result.replace( + new RegExp(placeholder, 'g'), + () => `${codeBlocks.shift()}` || '', + ); + + return result; +} diff --git a/packages/typedoc-plugin-markdown/src/theme/resources/partials/comments.comment.ts b/packages/typedoc-plugin-markdown/src/theme/resources/partials/comments.comment.ts index fac6e611a..1ae04a6a7 100644 --- a/packages/typedoc-plugin-markdown/src/theme/resources/partials/comments.comment.ts +++ b/packages/typedoc-plugin-markdown/src/theme/resources/partials/comments.comment.ts @@ -70,7 +70,7 @@ export function comment( const tagMd = [ opts.headingLevel ? heading(opts.headingLevel, tagText) + '\n' - : bold(tagText), + : bold(tagText) + '\n', ]; tagMd.push(this.partials.commentParts(tag.content)); return tagMd.join('\n'); diff --git a/packages/typedoc-plugin-markdown/test/fixtures/src/reflections/interfaces.ts b/packages/typedoc-plugin-markdown/test/fixtures/src/reflections/interfaces.ts index 056fbde57..3d43fa8c7 100644 --- a/packages/typedoc-plugin-markdown/test/fixtures/src/reflections/interfaces.ts +++ b/packages/typedoc-plugin-markdown/test/fixtures/src/reflections/interfaces.ts @@ -155,3 +155,39 @@ export interface InterfaceWithFlags { /** @internal */ internalProp: string; } + +/** + * Comments for interface + * over two lines + * + * And some more comments + * + * @typeParam A This is a parameter. + * + * @typeParam B Comments for a parameter. + * This sentence is on a soft new line. + * + * @typeParam C This is a parameter. + * + * Documentation with a double line + * + * @typeParam D + *

    These are comments with paras

    + *

    These are comments with paras

    + * Other comments + * Comments with

    paras

    + * + *

    These are comments with paras

    + */ +export interface InterfaceWithComments { + /** + * Some text. + * + * - list item + * - list item + * @deprecated This is a deprecated property + * + * @see https://example.com + */ + propertyWithComments: string; +} diff --git a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/comments.spec.ts.snap b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/comments.spec.ts.snap index 011e9ba5a..50a4d39b7 100644 --- a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/comments.spec.ts.snap +++ b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/comments.spec.ts.snap @@ -284,7 +284,7 @@ This is a simple example on how to use include. | Enumeration Member | Value | Description | | :------ | :------ | :------ | -| \`Member\` | \`0\` | Comment for Member

    Some

    html

    and .

    **Deprecated**
    Deprecated member

    **See**
    [SameName](README.md#samename-1) | +| \`Member\` | \`0\` |

    Comment for Member

    Some

    html

    and .

    **Deprecated**

    Deprecated member

    **See**

    [SameName](README.md#samename-1)

    | | \`MemberB\` | \`1\` | - | ## Interfaces @@ -491,7 +491,7 @@ This is a simple example on how to use include. | Enumeration Member | Value | Description | | :------ | :------ | :------ | -| \`Member\` | \`0\` | Comment for Member

    Some \\ html \\ and \\\\.

    **Deprecated**
    Deprecated member

    **See**
    [SameName](/some-path/README.mdx#SameName-1) | +| \`Member\` | \`0\` |

    Comment for Member

    Some \\ html \\ and \\\\.

    **Deprecated**

    Deprecated member

    **See**

    [SameName](/some-path/README.mdx#SameName-1)

    | | \`MemberB\` | \`1\` | - | ## Interfaces diff --git a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/navigation.spec.ts.snap b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/navigation.spec.ts.snap index 01ed49792..fec516060 100644 --- a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/navigation.spec.ts.snap +++ b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/navigation.spec.ts.snap @@ -2849,6 +2849,11 @@ exports[`Navigation should gets Navigation Json for single entry point: (Output "kind": 256, "path": "interfaces/IndexableInterface.md" }, + { + "title": "InterfaceWithComments", + "kind": 256, + "path": "interfaces/InterfaceWithComments.md" + }, { "title": "InterfaceWithEventProperties", "kind": 256, diff --git a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/objects-and-params.spec.ts.snap b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/objects-and-params.spec.ts.snap index 364ead821..14820267c 100644 --- a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/objects-and-params.spec.ts.snap +++ b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/objects-and-params.spec.ts.snap @@ -204,7 +204,7 @@ Comments for BasicInterface | Property | Type | Description | | :------ | :------ | :------ | -| ~~\`deprecatedProp\`~~ | \`string\` | **Deprecated**
    This prop is deprecated

    **Some Tag**
    Comments for some tag | +| ~~\`deprecatedProp\`~~ | \`string\` |

    **Deprecated**

    This prop is deprecated

    **Some Tag**

    Comments for some tag

    | | \`functionProp\` | (\`s\`: \`string\`) => \`boolean\` | Comments for functionProper | | \`optionalProp?\` | \`string\` | Comments for optional prop | | \`prop\` | \`string\` | Comments for prop | diff --git a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.class.spec.ts.snap b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.class.spec.ts.snap index f1dde7555..cbc3aaabd 100644 --- a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.class.spec.ts.snap +++ b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.class.spec.ts.snap @@ -1010,9 +1010,9 @@ new ClassWithSimpleProps(): ClassWithSimpleProps | Property | Type | Default value | Description | | :------ | :------ | :------ | :------ | | \`propA\` | \`string\` | \`'propAValue'\` | Comments for propA | -| \`propB\` | \`string\` | \`'propBDefaultValue'\` | Comments for propB | -| \`propC\` | \`string\` | \`'propCDefaultValue'\` | Comments for propB
    on two lines | -| \`propD\` | \`string\` | \`undefined\` | Comments for propE

    **Tag**
    SomeTag | +| \`propB\` | \`string\` | 'propBDefaultValue' |

    Comments for propB

    | +| \`propC\` | \`string\` | 'propCDefaultValue' |

    Comments for propB on two lines

    | +| \`propD\` | \`string\` | \`undefined\` |

    Comments for propE

    **Tag**

    SomeTag

    | " `; diff --git a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.interface.spec.ts.snap b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.interface.spec.ts.snap index 729433f3f..493df5235 100644 --- a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.interface.spec.ts.snap +++ b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.interface.spec.ts.snap @@ -244,7 +244,7 @@ Comments for BasicInterface | Property | Type | Description | | :------ | :------ | :------ | -| ~~\`deprecatedProp\`~~ | \`string\` | **Deprecated**
    This prop is deprecated

    **Some Tag**
    Comments for some tag | +| ~~\`deprecatedProp\`~~ | \`string\` |

    **Deprecated**

    This prop is deprecated

    **Some Tag**

    Comments for some tag

    | | \`functionProp\` | (\`s\`: \`string\`) => \`boolean\` | Comments for functionProper | | \`optionalProp?\` | \`string\` | Comments for optional prop | | \`prop\` | \`string\` | Comments for prop | @@ -559,7 +559,7 @@ Comments for ExtendedInterface | Property | Type | Description | Inherited from | | :------ | :------ | :------ | :------ | -| ~~\`deprecatedProp\`~~ | \`string\` | **Deprecated**
    This prop is deprecated

    **Some Tag**
    Comments for some tag | [\`BasicInterface\`](BasicInterface.md).\`deprecatedProp\` | +| ~~\`deprecatedProp\`~~ | \`string\` |

    **Deprecated**

    This prop is deprecated

    **Some Tag**

    Comments for some tag

    | [\`BasicInterface\`](BasicInterface.md).\`deprecatedProp\` | | \`extendedProp\` | \`string\` | - | - | | \`functionProp\` | (\`s\`: \`string\`) => \`boolean\` | Comments for functionProper | [\`BasicInterface\`](BasicInterface.md).\`functionProp\` | | \`optionalProp?\` | \`string\` | Comments for optional prop | [\`BasicInterface\`](BasicInterface.md).\`optionalProp\` | @@ -619,6 +619,90 @@ Comments for IndexableInterface " `; +exports[`Interface Reflection should compile interface with comments: (Output File Strategy "members") (Option Group "1") 1`] = ` +"# Interface: InterfaceWithComments\\ + +Comments for interface +over two lines + +And some more comments + +## Type parameters + +• **A** + +This is a parameter. + +• **B** + +Comments for a parameter. +This sentence is on a soft new line. + +• **C** + +This is a parameter. + + Documentation with a double line + +• **D** + +

    These are comments with paras

    +

    These are comments with paras

    +Other comments +Comments with

    paras

    + +

    These are comments with paras

    + +## Properties + +### ~~propertyWithComments~~ + +> **propertyWithComments**: \`string\` + +Some text. + +- list item +- list item + +#### Deprecated + +This is a deprecated property + +#### See + +https://example.com + +#### Source + +[interfaces.ts:192](http://source-url) +" +`; + +exports[`Interface Reflection should compile interface with comments: (Output File Strategy "members") (Option Group "2") 1`] = ` +"# Interface: InterfaceWithComments\\ + +Comments for interface +over two lines + +And some more comments + +## Type parameters + +| Type parameter | Description | +| :------ | :------ | +| \`A\` | This is a parameter. | +| \`B\` | Comments for a parameter. This sentence is on a soft new line. | +| \`C\` |

    This is a parameter.

    Documentation with a double line

    | +| \`D\` |

    These are comments with paras

    These are comments with paras

    Other comments Comments with

    paras

    These are comments with paras

    | + +## Properties + +| Property | Type | Description | +| :------ | :------ | :------ | +| ~~\`propertyWithComments\`~~ | \`string\` |

    Some text.

    • list item
    • list item

    **Deprecated**

    This is a deprecated property

    **See**

    https://example.com

    | +" +`; + exports[`Interface Reflection should compile interface with event properties: (Output File Strategy "members") (Option Group "1") 1`] = ` "# Interface: InterfaceWithEventProperties @@ -686,7 +770,7 @@ exports[`Interface Reflection should compile interface with event properties: (O | Event | Type | Description | | :------ | :------ | :------ | | \`anotherEvent\` | \`MouseEvent\` | - | -| ~~\`someEvent?\`~~ | (\`eventParam\`: [\`CustomEventInterface\`](CustomEventInterface.md)\\<\`MouseEvent\`\\>) => \`void\` | Description for event someEvent

    **Deprecated**
    Deprectaed comments | +| ~~\`someEvent?\`~~ | (\`eventParam\`: [\`CustomEventInterface\`](CustomEventInterface.md)\\<\`MouseEvent\`\\>) => \`void\` |

    Description for event someEvent

    **Deprecated**

    Deprectaed comments

    | " `; diff --git a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/urls.spec.ts.snap b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/urls.spec.ts.snap index ee34b2a81..9c64bf065 100644 --- a/packages/typedoc-plugin-markdown/test/specs/__snapshots__/urls.spec.ts.snap +++ b/packages/typedoc-plugin-markdown/test/specs/__snapshots__/urls.spec.ts.snap @@ -541,6 +541,7 @@ exports[`Urls should gets Urls for single entry points: outputFileStrategy: memb "interfaces/CustomEventInterface.md", "interfaces/ExtendedInterface.md", "interfaces/IndexableInterface.md", + "interfaces/InterfaceWithComments.md", "interfaces/InterfaceWithEventProperties.md", "interfaces/InterfaceWithFlags.md", "interfaces/InterfaceWithTypeParameters.md", @@ -616,6 +617,7 @@ exports[`Urls should gets Urls for single entry points: outputFileStrategy: memb "interfaces/CustomEventInterface.md", "interfaces/ExtendedInterface.md", "interfaces/IndexableInterface.md", + "interfaces/InterfaceWithComments.md", "interfaces/InterfaceWithEventProperties.md", "interfaces/InterfaceWithFlags.md", "interfaces/InterfaceWithTypeParameters.md", diff --git a/packages/typedoc-plugin-markdown/test/specs/reflection.interface.spec.ts b/packages/typedoc-plugin-markdown/test/specs/reflection.interface.spec.ts index b5d23f5df..4d792ee22 100644 --- a/packages/typedoc-plugin-markdown/test/specs/reflection.interface.spec.ts +++ b/packages/typedoc-plugin-markdown/test/specs/reflection.interface.spec.ts @@ -44,4 +44,12 @@ describe(`Interface Reflection`, () => { 'interfaces/InterfaceWithFlags.md', ); }); + + test(`should compile interface with comments`, () => { + expectFileToEqual( + 'reflections', + 'members', + 'interfaces/InterfaceWithComments.md', + ); + }); });