Skip to content

Commit

Permalink
fix(core): normalize line breaks and tags within table columns (#615)
Browse files Browse the repository at this point in the history
  • Loading branch information
tgreyuk committed May 14, 2024
1 parent 8962a9f commit 62062af
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -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<br />a newline, \\| a pipe, and a code block:<br />`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 <br /> tags', () => {
const input = 'This is a string with a trailing <br /> tag<br /> ';
const expectedOutput = 'This is a string with a trailing <br /> 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 = '<ol><li>First item</li><li>Second item</li></ol>';
expect(formatTableColumn(input)).toBe(expectedOutput);
});
});
Original file line number Diff line number Diff line change
@@ -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, '<br />')
.replace(/\`\`\`ts/g, '`')
.replace(/\`\`\`/g, '`')
.replace(/\n/g, '')
.replace(/(<br \/>\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, '\\|');
}
2 changes: 2 additions & 0 deletions packages/typedoc-plugin-markdown/src/libs/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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
<h4>Subheading</h4>
- list item 1
- list item 2`;

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>`;

expect(markdownBlocksToHtml(input)).toBe(expectedOutput);
});

it('should correctly convert markdown to HTML', () => {
const input = `<p>paragraph</p>
New line
<p>paragraph</p>
<p>
paragraph with new line
</p>`;

const expectedOutput = `<p>paragraph</p><p>New line</p><p>paragraph</p><p>paragraph with new line </p>`;

expect(markdownBlocksToHtml(input)).toBe(expectedOutput);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export function markdownBlocksToHtml(markdownText: string) {
// Remove new lines inside <p> tags
markdownText = markdownText.replace(/<p>([\s\S]*?)<\/p>/gm, (match, p1) => {
const contentWithoutNewLinesOrLeadingSpaces = p1
.replace(/\r?\n|\r/g, ' ')
.replace(/^\s+/, '');
return `<p>${contentWithoutNewLinesOrLeadingSpaces}</p>`;
});

// Replace headers
markdownText = markdownText.replace(
/^(#{1,6})\s*(.*?)\s*$/gm,
(match, p1, p2) => {
const level = p1.length;
return `<h${level}>${p2}</h${level}>`;
},
);

// Replace triple code blocks with code
markdownText = markdownText.replace(
/```.*?\n([\s\S]*?)```/gs,
'<code>$1</code>',
);

// Replace horizontal rules
markdownText = markdownText.replace(/^[-*_]{3,}\s*$/gm, '<hr />');

// Replace unordered lists
markdownText = markdownText.replace(/^(\s*-\s+.+$(\r?\n)?)+/gm, (match) => {
const items = match.trim().split('\n');
const listItems = items
.map((item) => `<li>${item.trim().substring(2)}</li>`)
.join('');
return `<ul>${listItems}</ul>`;
});

// Replace ordered lists
markdownText = markdownText.replace(
/^(\s*\d+\.\s+.+$(\r?\n)?)+/gm,
(match) => {
const items = match.trim().split('\n');
const listItems = items
.map(
(item) => `<li>${item.trim().substring(item.indexOf('.') + 2)}</li>`,
)
.join('');
return `<ol>${listItems}</ol>`;
},
);

// Replace paragraphs
markdownText = markdownText.replace(
/^(?!.*<[^>]+>)(.+?)(?:(?:\r\n|\r|\n){2,}|$)(?!.*<[^>]+>)/gm,
'<p>$1</p>',
);

// Replace ordered lists
markdownText = markdownText.replace(
/^(\s*\d+\.\s+.+$(\r?\n)?)+/gm,
(match) => {
const items = match.trim().split('\n');
const listItems = items
.map(
(item) => `<li>${item.trim().substring(item.indexOf('.') + 1)}</li>`,
)
.join('');
return `<ol>${listItems}</ol>`;
},
);

// Finally remove all new lines
return markdownText.replace(/\n/g, '');
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <p>These are comments with paras</p>
* <p>These are comments with paras</p>
* Other comments
* Comments with <p>paras</p>
*
* <p>These are comments with paras</p>
*/
export interface InterfaceWithComments<A, B, C, D> {
/**
* Some text.
*
* - list item
* - list item
* @deprecated This is a deprecated property
*
* @see https://example.com
*/
propertyWithComments: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ This is a simple example on how to use include.
| Enumeration Member | Value | Description |
| :------ | :------ | :------ |
| \`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) |
| \`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> |
| \`MemberB\` | \`1\` | - |
## Interfaces
Expand Down Expand Up @@ -491,7 +491,7 @@ This is a simple example on how to use include.
| Enumeration Member | Value | Description |
| :------ | :------ | :------ |
| <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) |
| <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> |
| <a id="MemberB" name="MemberB"></a> \`MemberB\` | \`1\` | - |
## Interfaces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ Comments for BasicInterface
| Property | Type | Description |
| :------ | :------ | :------ |
| ~~\`deprecatedProp\`~~ | \`string\` | **Deprecated**<br />This prop is deprecated<br /><br />**Some Tag**<br />Comments for some tag |
| ~~\`deprecatedProp\`~~ | \`string\` | <p>**Deprecated**</p><p>This prop is deprecated</p><p>**Some Tag**</p><p>Comments for some tag</p> |
| \`functionProp\` | (\`s\`: \`string\`) => \`boolean\` | Comments for functionProper |
| \`optionalProp?\` | \`string\` | Comments for optional prop |
| \`prop\` | \`string\` | Comments for prop |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<br />on two lines |
| \`propD\` | \`string\` | \`undefined\` | Comments for propE<br /><br />**Tag**<br />SomeTag |
| \`propB\` | \`string\` | <code>'propBDefaultValue'</code> | <p>Comments for propB</p> |
| \`propC\` | \`string\` | <code>'propCDefaultValue'</code> | <p>Comments for propB on two lines</p> |
| \`propD\` | \`string\` | \`undefined\` | <p>Comments for propE</p><p>**Tag**</p><p>SomeTag</p> |
"
`;

Expand Down

0 comments on commit 62062af

Please sign in to comment.