Skip to content

Commit

Permalink
feat: add support to convert tables from md to rich text (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cezar Sampaio authored Nov 4, 2021
1 parent 626fa80 commit 213a29c
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 10 deletions.
3 changes: 3 additions & 0 deletions packages/rich-text-from-markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ The library will convert automatically the following markdown nodes:
- `blockquote`
- `list`
- `listItem`
- `table`
- `tableRow`
- `tableCell`

If the markdown content has unsupported nodes like image `![image](url)` you can add a callback as a second argument
and it will get called everytime an unsupported node is found. The return value of the callback will be the rich text representation
Expand Down
37 changes: 37 additions & 0 deletions packages/rich-text-from-markdown/src/__test__/real-world.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
##### h5 Heading
###### h6 Heading

## Paragraphs

This is a paragraph
with a new line.

This is a new paragraph.

This is a paragraph<br/>using br.


## Horizontal Rules

Expand Down Expand Up @@ -70,3 +79,31 @@ Inline `code`
[link text](https://www.contentful.com)

[link with title](https://www.contentful.com/blog/ "title text!")

## Tables

| Name | Country |
| -------------------------------------------- | ------- |
| Test 1 | Germany |
| Test 2 | USA |
| > Test 3 | USA |
| * Test 4 | Germany |
| # Test 5 | Germany |
| <p>Test 6<br/>Test 7</p> | USA |
| <ul><li>Test 8</li></ul> | USA |
| <blockquote>Test 9</blockquote> | Germany |
| <div><p>Test 10</p> and <p>Test 11</p></div> | Germany |
| <img src="image.jpg" /> | Germany |
| ![Image Description](image.jpg) | Brazil |
| **[Test 12](https://example.com)** | USA |

## Tables with marks

| **Bold Header 1** | **Bold Header 2** |
| ----------------- | ----------------- |
| _Italic_ | `Code` |

## Tables without body

| abc | def |
| --- | --- |
173 changes: 169 additions & 4 deletions packages/rich-text-from-markdown/src/__test__/real-world.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,27 @@ describe('rich-text-from-markdown', () => {
block(BLOCKS.HEADING_4, {}, text('h4 Heading')),
block(BLOCKS.HEADING_5, {}, text('h5 Heading')),
block(BLOCKS.HEADING_6, {}, text('h6 Heading')),
// Paragraphs
block(BLOCKS.HEADING_2, {}, text('Paragraphs')),
block(
BLOCKS.PARAGRAPH,
{},
text(`This is a paragraph
with a new line.`),
),
block(BLOCKS.PARAGRAPH, {}, text('This is a new paragraph.')),
// TODO: <br /> test should be ideally the same as the new line one.
block(BLOCKS.PARAGRAPH, {}, text('This is a paragraph'), text('using br.')),

block(BLOCKS.HEADING_2, {}, text('Horizontal Rules')),
block(BLOCKS.HR),
block(BLOCKS.HR),
block(BLOCKS.HR),
block(BLOCKS.HEADING_2, {}, text('Emphasis')),
block(BLOCKS.PARAGRAPH, {}, text('This is bold text', mark('bold'))),
block(BLOCKS.PARAGRAPH, {}, text('This is bold text', mark('bold'))),
block(BLOCKS.PARAGRAPH, {}, text('This is italic text', mark('italic'))),
block(BLOCKS.PARAGRAPH, {}, text('This is italic text', mark('italic'))),
block(BLOCKS.PARAGRAPH, {}, text('This is bold text', mark(MARKS.BOLD))),
block(BLOCKS.PARAGRAPH, {}, text('This is bold text', mark(MARKS.BOLD))),
block(BLOCKS.PARAGRAPH, {}, text('This is italic text', mark(MARKS.ITALIC))),
block(BLOCKS.PARAGRAPH, {}, text('This is italic text', mark(MARKS.ITALIC))),
block(BLOCKS.PARAGRAPH, {}, text('Strikethrough is not supported')),
block(BLOCKS.HEADING_2, {}, text('Blockquotes')),
block(BLOCKS.QUOTE, {}, block(BLOCKS.PARAGRAPH, {}, text('Blockquotes'))),
Expand Down Expand Up @@ -158,6 +170,159 @@ describe('rich-text-from-markdown', () => {
text('link with title'),
),
),
// Tables
block(BLOCKS.HEADING_2, {}, text('Tables')),
block(
BLOCKS.TABLE,
{},
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Name'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Country'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Test 1'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Germany'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Test 2'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('USA'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('> Test 3'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('USA'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('* Test 4'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Germany'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('# Test 5'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Germany'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(
BLOCKS.TABLE_CELL,
{},
block(BLOCKS.PARAGRAPH, {}, text('Test 6')),
block(BLOCKS.PARAGRAPH, {}, text('Test 7')),
),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('USA'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Test 8'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('USA'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Test 9'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Germany'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(
BLOCKS.TABLE_CELL,
{},
block(BLOCKS.PARAGRAPH, {}, text('Test 10')),
block(BLOCKS.PARAGRAPH, {}, text(' and ')),
block(BLOCKS.PARAGRAPH, {}, text('Test 11')),
),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Germany'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Germany'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('Brazil'))),
),
block(
BLOCKS.TABLE_ROW,
{},
block(
BLOCKS.TABLE_CELL,
{},
block(
BLOCKS.PARAGRAPH,
{},
inline(
INLINES.HYPERLINK,
{ data: { uri: 'https://example.com' } },
text('Test 12', mark(MARKS.BOLD)),
),
),
),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('USA'))),
),
),
// Tables with marks
block(BLOCKS.HEADING_2, {}, text('Tables with marks')),
block(
BLOCKS.TABLE,
{},
block(
BLOCKS.TABLE_ROW,
{},
block(
BLOCKS.TABLE_CELL,
{},
block(BLOCKS.PARAGRAPH, {}, text('Bold Header 1', mark(MARKS.BOLD))),
),
block(
BLOCKS.TABLE_CELL,
{},
block(BLOCKS.PARAGRAPH, {}, text('Bold Header 2', mark(MARKS.BOLD))),
),
),
block(
BLOCKS.TABLE_ROW,
{},
block(
BLOCKS.TABLE_CELL,
{},
block(BLOCKS.PARAGRAPH, {}, text('Italic', mark(MARKS.ITALIC))),
),
block(
BLOCKS.TABLE_CELL,
{},
block(BLOCKS.PARAGRAPH, {}, text('Code', mark(MARKS.CODE))),
),
),
),
// Tables without body
block(BLOCKS.HEADING_2, {}, text('Tables without body')),
block(
BLOCKS.TABLE,
{},
block(
BLOCKS.TABLE_ROW,
{},
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('abc'))),
block(BLOCKS.TABLE_CELL, {}, block(BLOCKS.PARAGRAPH, {}, text('def'))),
),
),
),
);
});
Expand Down
61 changes: 55 additions & 6 deletions packages/rich-text-from-markdown/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const markdownNodeTypes = new Map<string, string>([
['blockquote', BLOCKS.QUOTE],
['list', 'list'],
['listItem', BLOCKS.LIST_ITEM],
['table', BLOCKS.TABLE],
['tableRow', BLOCKS.TABLE_ROW],
['tableCell', BLOCKS.TABLE_CELL],
]);

const nodeTypeFor = (node: MarkdownNode) => {
Expand All @@ -42,7 +45,11 @@ const nodeTypeFor = (node: MarkdownNode) => {
}
};

const markTypes = new Map([['emphasis', 'italic'], ['strong', 'bold'], ['inlineCode', 'code']]);
const markTypes = new Map([
['emphasis', 'italic'],
['strong', 'bold'],
['inlineCode', 'code'],
]);
const markTypeFor = (node: MarkdownNode) => {
return markTypes.get(node.type);
};
Expand All @@ -65,6 +72,10 @@ const nodeContainerTypes = new Map([
[BLOCKS.QUOTE, 'block'],
[BLOCKS.HR, 'block'],
[BLOCKS.PARAGRAPH, 'block'],
[BLOCKS.TABLE, 'block'],
[BLOCKS.TABLE_CELL, 'block'],
[BLOCKS.TABLE_HEADER_CELL, 'block'],
[BLOCKS.TABLE_ROW, 'block'],
[INLINES.HYPERLINK, 'inline'],
['text', 'text'],
['emphasis', 'text'],
Expand All @@ -84,6 +95,10 @@ const isInline = (nodeType: string) => {
return nodeContainerTypes.get(nodeType) === 'inline';
};

const isTableCell = (nodeType: string) => {
return nodeType === BLOCKS.TABLE_CELL;
};

const buildHyperlink = async (
node: MarkdownLinkNode,
fallback: FallbackResolver,
Expand Down Expand Up @@ -117,6 +132,33 @@ const buildGenericBlockOrInline = async (
];
};

const buildTableCell = async (
node: MarkdownNode,
fallback: FallbackResolver,
appliedMarksTypes: string[],
): Promise<Array<Block>> => {
const content = await mdToRichTextNodes(node.children, fallback, appliedMarksTypes);

/**
* We should only support texts inside table cells.
* Some markdowns might contain html inside tables such as <ul>, <blockquote>, etc
* but they are pretty much filtered out by markdownNodeTypes and nodeContainerTypes variables.
* so we ended up receiving only `text` nodes.
* We can't have table cells with text nodes directly, we must wrap text nodes inside paragraphs.
*/
return [
{
nodeType: BLOCKS.TABLE_CELL,
content: content.map(contentNode => ({
nodeType: BLOCKS.PARAGRAPH,
data: {},
content: [contentNode],
})),
data: {},
} as Block,
];
};

const buildText = async (
node: MarkdownNode,
fallback: FallbackResolver,
Expand Down Expand Up @@ -148,7 +190,6 @@ const buildText = async (
const buildFallbackNode = async (
node: MarkdownNode,
fallback: FallbackResolver,
appliedMarksTypes: string[],
): Promise<Node[]> => {
const fallbackResult = await fallback(node);

Expand All @@ -167,13 +208,21 @@ async function mdToRichTextNode(

if (isLink(node)) {
return await buildHyperlink(node, fallback, appliedMarksTypes);
} else if (isBlock(nodeType) || isInline(nodeType)) {
}

if (isTableCell(nodeType)) {
return await buildTableCell(node, fallback, appliedMarksTypes);
}

if (isBlock(nodeType) || isInline(nodeType)) {
return await buildGenericBlockOrInline(node, fallback, appliedMarksTypes);
} else if (isText(nodeType)) {
}

if (isText(nodeType)) {
return await buildText(node, fallback, appliedMarksTypes);
} else {
return await buildFallbackNode(node, fallback, appliedMarksTypes);
}

return await buildFallbackNode(node, fallback);
}

async function mdToRichTextNodes(
Expand Down

0 comments on commit 213a29c

Please sign in to comment.