diff --git a/README.md b/README.md index 93d7ae6..aa262b6 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,21 @@ objects. ### Supported Markdown Elements -* All inline elements (italics, bold, strikethrough, inline code, hyperlinks) -* Lists (ordered, unordered, checkboxes) -* All headers -* Code blocks -* Block quotes (with some limitations) -* Images -* Thematic Breaks / Dividers +- All inline elements (italics, bold, strikethrough, inline code, hyperlinks) +- Lists (ordered, unordered, checkboxes) +- All headers +- Code blocks +- Block quotes (with some limitations) +- Images +- Thematic Breaks / Dividers +- Tables (alignment not preserved) + +### Not Yet Supported Markdown Elements + +- - Block quotes (limited functionality; does not support lists, headings, or images within the block quote) ## Installation + ``` npm install @instantish/mack ``` @@ -29,7 +35,6 @@ npm install @instantish/mack ```ts import {markdownToBlocks} from '@instantish/mack'; - const blocks = markdownToBlocks(` # Hello world @@ -39,7 +44,7 @@ const blocks = markdownToBlocks(` abc _123_ ![cat](https://images.unsplash.com/photo-1574158622682-e40e69881006) -`) +`); ``` The `blocks` object now results in [this](https://app.slack.com/block-kit-builder/T01BFUV9UPJ#%7B%22blocks%22:%5B%7B%22text%22:%7B%22text%22:%22Hello%20world%22,%22type%22:%22plain_text%22%7D,%22type%22:%22header%22%7D,%7B%22text%22:%7B%22text%22:%22•%20bulleted%20item%201%5Cn•%20bulleted%20item%202%22,%22type%22:%22mrkdwn%22%7D,%22type%22:%22section%22%7D,%7B%22text%22:%7B%22text%22:%22abc%20_123_%22,%22type%22:%22mrkdwn%22%7D,%22type%22:%22section%22%7D,%7B%22alt_text%22:%22cat%22,%22image_url%22:%22https://images.unsplash.com/photo-1574158622682-e40e69881006?w=640%22,%22type%22:%22image%22%7D%5D%7D) payload. @@ -47,8 +52,9 @@ The `blocks` object now results in [this](https://app.slack.com/block-kit-builde ## API `function markdownToBlocks(text: string, options: ParsingOptions): KnownBlock[]` -* `text`: the content to parse -* `options`: the options to use when parsing. + +- `text`: the content to parse +- `options`: the options to use when parsing. ### Parsing Options diff --git a/src/parser/internal.ts b/src/parser/internal.ts index 1aded08..0d2e796 100644 --- a/src/parser/internal.ts +++ b/src/parser/internal.ts @@ -86,6 +86,18 @@ function addMrkdwn( } } +function parsePhrasingContentToStrings( + element: md.PhrasingContent, + accumulator: String[] +) { + if (element.type === 'image') { + accumulator.push(element.url ?? element.title ?? element.alt ?? 'image'); + } else { + const text = parseMrkdwn(element); + accumulator.push(text); + } +} + function parsePhrasingContent( element: md.PhrasingContent, accumulator: (SectionBlock | ImageBlock)[], @@ -173,6 +185,55 @@ function parseList(element: md.List, options: ListOptions = {}): SectionBlock { }; } +function combineBetweenPipes(texts: String[]): String { + return `| ${texts.join(' | ')} |`; +} + +function parseTableRows(rows: md.TableRow[]): String[] { + const parsedRows: String[] = []; + rows.forEach((row, index) => { + const parsedCells = parseTableRow(row); + if (index === 1) { + const headerRowArray = new Array(parsedCells.length).fill('---'); + const headerRow = combineBetweenPipes(headerRowArray); + parsedRows.push(headerRow); + } + parsedRows.push(combineBetweenPipes(parsedCells)); + }); + return parsedRows; +} + +function parseTableRow(row: md.TableRow): String[] { + const parsedCells: String[] = []; + row.children.forEach(cell => { + parsedCells.push(parseTableCell(cell)); + }); + return parsedCells; +} + +function parseTableCell(cell: md.TableCell): String { + const texts = cell.children.reduce( + (accumulator: String[], child: md.PhrasingContent) => { + parsePhrasingContentToStrings(child, accumulator); + return accumulator; + }, + [] as String[] + ); + return texts.join(' '); +} + +function parseTable(element: md.Table): SectionBlock { + const parsedRows = parseTableRows(element.children); + + return { + type: 'section', + text: { + type: 'mrkdwn', + text: `\`\`\`\n${parsedRows.join('\n')}\n\`\`\``, + }, + }; +} + function parseBlockquote(node: md.Blockquote): KnownBlock[] { return node.children .filter((child): child is md.Paragraph => child.type === 'paragraph') @@ -205,6 +266,9 @@ function parseNode( case 'list': return [parseList(node, options.lists)]; + case 'table': + return [parseTable(node)]; + case 'thematicBreak': return [parseThematicBreak()]; diff --git a/test/integration.spec.ts b/test/integration.spec.ts index cb08090..dce607e 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -25,6 +25,11 @@ a **b** _c_ **_d_ e** - [ ] checkbox false - [x] checkbox true + +| Syntax | Description | +| ----------- | ----------- | +| Header | Title | +| Paragraph | Text | `; const actual = await markdownToBlocks(text); @@ -45,6 +50,14 @@ a **b** _c_ **_d_ e** slack.section('• bullet _a_\n• bullet _b_'), slack.section('1. number _a_\n2. number _b_'), slack.section('• checkbox false\n• checkbox true'), + slack.section( + '```\n' + + '| Syntax | Description |\n' + + '| --- | --- |\n' + + '| Header | Title |\n' + + '| Paragraph | Text |\n' + + '```' + ), ]; console.log(JSON.stringify(expected, null, 3));