Skip to content

Commit 62062af

Browse files
committed
fix(core): normalize line breaks and tags within table columns (#615)
1 parent 8962a9f commit 62062af

16 files changed

+337
-32
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
import { formatTableColumn } from './format-table-column';
22

33
describe('formatTableColumn', () => {
4-
it('should format table column correctly', () => {
5-
const input = `This is a string with
6-
a newline, | a pipe, and a code block:
7-
\`\`\`ts
8-
const x = 10;
9-
\`\`\``;
10-
const expectedOutput =
11-
'This is a string with<br />a newline, \\| a pipe, and a code block:<br />`const x = 10;`';
12-
const result = formatTableColumn(input);
13-
expect(result).toEqual(expectedOutput);
4+
it('should correctly escape pipes', () => {
5+
const input = 'This is a test | with a pipe.';
6+
const expectedOutput = 'This is a test \\| with a pipe.';
7+
expect(formatTableColumn(input)).toBe(expectedOutput);
148
});
159

16-
it('should remove trailing <br /> tags', () => {
17-
const input = 'This is a string with a trailing <br /> tag<br /> ';
18-
const expectedOutput = 'This is a string with a trailing <br /> tag';
19-
const result = formatTableColumn(input);
20-
expect(result).toEqual(expectedOutput);
10+
it('should correctly convert multi-line markdown to HTML', () => {
11+
const input = `1. First item
12+
2. Second item`;
13+
const expectedOutput = '<ol><li>First item</li><li>Second item</li></ol>';
14+
expect(formatTableColumn(input)).toBe(expectedOutput);
2115
});
2216
});
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { markdownBlocksToHtml } from './markdown-blocks-to-html';
2+
import { normalizeLineBreaks } from './normalize-line-breaks';
3+
14
export function formatTableColumn(str: string) {
2-
return str
3-
.replace(/\|/g, '\\|')
4-
.replace(/\n(?=(?:[^`]*`[^`]*`)*[^`]*$)/gi, '<br />')
5-
.replace(/\`\`\`ts/g, '`')
6-
.replace(/\`\`\`/g, '`')
7-
.replace(/\n/g, '')
8-
.replace(/(<br \/>\s*)+$/g, '');
5+
// Normalize line breaks
6+
let md = normalizeLineBreaks(str);
7+
// If comments are on multiple lines convert markdown block tags to HTML and remove new lines.
8+
if (md.split('\n').length > 1) {
9+
md = markdownBlocksToHtml(md);
10+
}
11+
// Finally return with escaped pipes
12+
return md.replace(/\|/g, '\\|');
913
}

Diff for: packages/typedoc-plugin-markdown/src/libs/utils/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export { formatMarkdown } from './format-markdown';
88
export { formatTableColumn } from './format-table-column';
99
export { getFileNameWithExtension } from './get-file-name-with-extension';
1010
export { isQuoted } from './is-quoted';
11+
export { markdownBlocksToHtml } from './markdown-blocks-to-html';
12+
export { normalizeLineBreaks } from './normalize-line-breaks';
1113
export { removeFirstScopedDirectory } from './remove-first-scoped-directory';
1214
export { removeLineBreaks } from './remove-line-breaks';
1315
export { sanitizeComments } from './sanitize-comments';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { markdownBlocksToHtml } from './markdown-blocks-to-html';
2+
3+
describe('markdownBlocksToHtml', () => {
4+
it('should correctly convert markdown to HTML', () => {
5+
const input = `This is a test
6+
7+
Double new line
8+
9+
### Heading
10+
11+
<h4>Subheading</h4>
12+
13+
- list item 1
14+
- list item 2`;
15+
16+
const expectedOutput = `<p>This is a test</p><p>Double new line</p><h3>Heading</h3><h4>Subheading</h4><ul><li>list item 1</li><li>list item 2</li></ul>`;
17+
18+
expect(markdownBlocksToHtml(input)).toBe(expectedOutput);
19+
});
20+
21+
it('should correctly convert markdown to HTML', () => {
22+
const input = `<p>paragraph</p>
23+
24+
New line
25+
26+
<p>paragraph</p>
27+
<p>
28+
paragraph with new line
29+
</p>`;
30+
31+
const expectedOutput = `<p>paragraph</p><p>New line</p><p>paragraph</p><p>paragraph with new line </p>`;
32+
33+
expect(markdownBlocksToHtml(input)).toBe(expectedOutput);
34+
});
35+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
export function markdownBlocksToHtml(markdownText: string) {
2+
// Remove new lines inside <p> tags
3+
markdownText = markdownText.replace(/<p>([\s\S]*?)<\/p>/gm, (match, p1) => {
4+
const contentWithoutNewLinesOrLeadingSpaces = p1
5+
.replace(/\r?\n|\r/g, ' ')
6+
.replace(/^\s+/, '');
7+
return `<p>${contentWithoutNewLinesOrLeadingSpaces}</p>`;
8+
});
9+
10+
// Replace headers
11+
markdownText = markdownText.replace(
12+
/^(#{1,6})\s*(.*?)\s*$/gm,
13+
(match, p1, p2) => {
14+
const level = p1.length;
15+
return `<h${level}>${p2}</h${level}>`;
16+
},
17+
);
18+
19+
// Replace triple code blocks with code
20+
markdownText = markdownText.replace(
21+
/```.*?\n([\s\S]*?)```/gs,
22+
'<code>$1</code>',
23+
);
24+
25+
// Replace horizontal rules
26+
markdownText = markdownText.replace(/^[-*_]{3,}\s*$/gm, '<hr />');
27+
28+
// Replace unordered lists
29+
markdownText = markdownText.replace(/^(\s*-\s+.+$(\r?\n)?)+/gm, (match) => {
30+
const items = match.trim().split('\n');
31+
const listItems = items
32+
.map((item) => `<li>${item.trim().substring(2)}</li>`)
33+
.join('');
34+
return `<ul>${listItems}</ul>`;
35+
});
36+
37+
// Replace ordered lists
38+
markdownText = markdownText.replace(
39+
/^(\s*\d+\.\s+.+$(\r?\n)?)+/gm,
40+
(match) => {
41+
const items = match.trim().split('\n');
42+
const listItems = items
43+
.map(
44+
(item) => `<li>${item.trim().substring(item.indexOf('.') + 2)}</li>`,
45+
)
46+
.join('');
47+
return `<ol>${listItems}</ol>`;
48+
},
49+
);
50+
51+
// Replace paragraphs
52+
markdownText = markdownText.replace(
53+
/^(?!.*<[^>]+>)(.+?)(?:(?:\r\n|\r|\n){2,}|$)(?!.*<[^>]+>)/gm,
54+
'<p>$1</p>',
55+
);
56+
57+
// Replace ordered lists
58+
markdownText = markdownText.replace(
59+
/^(\s*\d+\.\s+.+$(\r?\n)?)+/gm,
60+
(match) => {
61+
const items = match.trim().split('\n');
62+
const listItems = items
63+
.map(
64+
(item) => `<li>${item.trim().substring(item.indexOf('.') + 1)}</li>`,
65+
)
66+
.join('');
67+
return `<ol>${listItems}</ol>`;
68+
},
69+
);
70+
71+
// Finally remove all new lines
72+
return markdownText.replace(/\n/g, '');
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { normalizeLineBreaks } from './normalize-line-breaks';
2+
3+
describe('normalizeLineBreaks', () => {
4+
it('should correctly concatenate lines', () => {
5+
const input = `This line should be concatenated with the next one.
6+
The next line.
7+
8+
This is the next line double break.
9+
- list item 1
10+
- list item 2
11+
12+
This is another test.`;
13+
14+
const expectedOutput = `This line should be concatenated with the next one. The next line.
15+
16+
This is the next line double break.
17+
- list item 1
18+
- list item 2
19+
20+
This is another test.`;
21+
22+
expect(normalizeLineBreaks(input)).toBe(expectedOutput);
23+
});
24+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export function normalizeLineBreaks(str: string): string {
2+
const codeBlocks: string[] = [];
3+
4+
const placeholder = '\n___CODEBLOCKPLACEHOLDER___\n';
5+
str = str.replace(/```[\s\S]*?```/g, (match) => {
6+
codeBlocks.push(match);
7+
return placeholder;
8+
});
9+
10+
const lines = str.split('\n');
11+
let result = '';
12+
for (let i = 0; i < lines.length; i++) {
13+
if (lines[i].length === 0) {
14+
result = result + lines[i] + '\n';
15+
} else {
16+
if (
17+
!lines[i].startsWith('#') &&
18+
lines[i + 1] &&
19+
/^[a-zA-Z`]/.test(lines[i + 1])
20+
) {
21+
result = result + lines[i] + ' ';
22+
} else {
23+
if (i < lines.length - 1) {
24+
result = result + lines[i] + '\n';
25+
} else {
26+
result = result + lines[i];
27+
}
28+
}
29+
}
30+
}
31+
32+
result = result.replace(
33+
new RegExp(placeholder, 'g'),
34+
() => `${codeBlocks.shift()}` || '',
35+
);
36+
37+
return result;
38+
}

Diff for: packages/typedoc-plugin-markdown/src/theme/resources/partials/comments.comment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function comment(
7070
const tagMd = [
7171
opts.headingLevel
7272
? heading(opts.headingLevel, tagText) + '\n'
73-
: bold(tagText),
73+
: bold(tagText) + '\n',
7474
];
7575
tagMd.push(this.partials.commentParts(tag.content));
7676
return tagMd.join('\n');

Diff for: packages/typedoc-plugin-markdown/test/fixtures/src/reflections/interfaces.ts

+36
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,39 @@ export interface InterfaceWithFlags {
155155
/** @internal */
156156
internalProp: string;
157157
}
158+
159+
/**
160+
* Comments for interface
161+
* over two lines
162+
*
163+
* And some more comments
164+
*
165+
* @typeParam A This is a parameter.
166+
*
167+
* @typeParam B Comments for a parameter.
168+
* This sentence is on a soft new line.
169+
*
170+
* @typeParam C This is a parameter.
171+
*
172+
* Documentation with a double line
173+
*
174+
* @typeParam D
175+
* <p>These are comments with paras</p>
176+
* <p>These are comments with paras</p>
177+
* Other comments
178+
* Comments with <p>paras</p>
179+
*
180+
* <p>These are comments with paras</p>
181+
*/
182+
export interface InterfaceWithComments<A, B, C, D> {
183+
/**
184+
* Some text.
185+
*
186+
* - list item
187+
* - list item
188+
* @deprecated This is a deprecated property
189+
*
190+
* @see https://example.com
191+
*/
192+
propertyWithComments: string;
193+
}

Diff for: packages/typedoc-plugin-markdown/test/specs/__snapshots__/comments.spec.ts.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ This is a simple example on how to use include.
284284
285285
| Enumeration Member | Value | Description |
286286
| :------ | :------ | :------ |
287-
| \`Member\` | \`0\` | Comment for Member<br /><br />Some <p> html </p> and <tag></tag>.<br /><br />**Deprecated**<br />Deprecated member<br /><br />**See**<br />[SameName](README.md#samename-1) |
287+
| \`Member\` | \`0\` | <p>Comment for Member</p>Some <p>html </p> and <tag></tag>.<p>**Deprecated**</p><p>Deprecated member</p><p>**See**</p><p>[SameName](README.md#samename-1)</p> |
288288
| \`MemberB\` | \`1\` | - |
289289
290290
## Interfaces
@@ -491,7 +491,7 @@ This is a simple example on how to use include.
491491
492492
| Enumeration Member | Value | Description |
493493
| :------ | :------ | :------ |
494-
| <a id="Member" name="Member"></a> \`Member\` | \`0\` | Comment for Member<br /><br />Some \\<p\\> html \\</p\\> and \\<tag\\>\\</tag\\>.<br /><br />**Deprecated**<br />Deprecated member<br /><br />**See**<br />[SameName](/some-path/README.mdx#SameName-1) |
494+
| <a id="Member" name="Member"></a> \`Member\` | \`0\` | <p>Comment for Member</p>Some \\<p\\> html \\</p\\> and \\<tag\\>\\</tag\\>.<p>**Deprecated**</p><p>Deprecated member</p><p>**See**</p><p>[SameName](/some-path/README.mdx#SameName-1)</p> |
495495
| <a id="MemberB" name="MemberB"></a> \`MemberB\` | \`1\` | - |
496496
497497
## Interfaces

Diff for: packages/typedoc-plugin-markdown/test/specs/__snapshots__/navigation.spec.ts.snap

+5
Original file line numberDiff line numberDiff line change
@@ -2849,6 +2849,11 @@ exports[`Navigation should gets Navigation Json for single entry point: (Output
28492849
"kind": 256,
28502850
"path": "interfaces/IndexableInterface.md"
28512851
},
2852+
{
2853+
"title": "InterfaceWithComments",
2854+
"kind": 256,
2855+
"path": "interfaces/InterfaceWithComments.md"
2856+
},
28522857
{
28532858
"title": "InterfaceWithEventProperties",
28542859
"kind": 256,

Diff for: packages/typedoc-plugin-markdown/test/specs/__snapshots__/objects-and-params.spec.ts.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ Comments for BasicInterface
204204
205205
| Property | Type | Description |
206206
| :------ | :------ | :------ |
207-
| ~~\`deprecatedProp\`~~ | \`string\` | **Deprecated**<br />This prop is deprecated<br /><br />**Some Tag**<br />Comments for some tag |
207+
| ~~\`deprecatedProp\`~~ | \`string\` | <p>**Deprecated**</p><p>This prop is deprecated</p><p>**Some Tag**</p><p>Comments for some tag</p> |
208208
| \`functionProp\` | (\`s\`: \`string\`) => \`boolean\` | Comments for functionProper |
209209
| \`optionalProp?\` | \`string\` | Comments for optional prop |
210210
| \`prop\` | \`string\` | Comments for prop |

Diff for: packages/typedoc-plugin-markdown/test/specs/__snapshots__/reflection.class.spec.ts.snap

+3-3
Original file line numberDiff line numberDiff line change
@@ -1010,9 +1010,9 @@ new ClassWithSimpleProps(): ClassWithSimpleProps
10101010
| Property | Type | Default value | Description |
10111011
| :------ | :------ | :------ | :------ |
10121012
| \`propA\` | \`string\` | \`'propAValue'\` | Comments for propA |
1013-
| \`propB\` | \`string\` | \`'propBDefaultValue'\` | Comments for propB |
1014-
| \`propC\` | \`string\` | \`'propCDefaultValue'\` | Comments for propB<br />on two lines |
1015-
| \`propD\` | \`string\` | \`undefined\` | Comments for propE<br /><br />**Tag**<br />SomeTag |
1013+
| \`propB\` | \`string\` | <code>'propBDefaultValue'</code> | <p>Comments for propB</p> |
1014+
| \`propC\` | \`string\` | <code>'propCDefaultValue'</code> | <p>Comments for propB on two lines</p> |
1015+
| \`propD\` | \`string\` | \`undefined\` | <p>Comments for propE</p><p>**Tag**</p><p>SomeTag</p> |
10161016
"
10171017
`;
10181018

0 commit comments

Comments
 (0)