From b08a9fcaa8cfe672ee5b8c5e2a4cc7b9336a3099 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 7 Jul 2017 18:28:15 -0400 Subject: [PATCH] improve Dropbox Paper paste handling --- src/components/Widgets/Markdown/unified.js | 53 ++++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 6a306e35..f91eb5e1 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -13,6 +13,9 @@ const remarkParseConfig = { fences: true }; const remarkStringifyConfig = { listItemIndent: '1', fences: true }; const rehypeParseConfig = { fragment: true }; +/** + * Remove empty nodes, including the top level parents of deeply nested empty nodes. + */ const rehypeRemoveEmpty = () => { const isVoidElement = node => ['img', 'hr'].includes(node.tagName); const isNonEmptyText = node => node.type === 'text' && node.value; @@ -37,6 +40,44 @@ const rehypeRemoveEmpty = () => { return transform; }; +/** + * If the first child of a list item is a list, include it in the previous list + * item. Otherwise it translates to markdown as having two bullets. When + * rehype-remark processes a list and finds children that are not list items, it + * wraps them in list items, which leads to the condition this plugin addresses. + * Dropbox Paper currently outputs this kind of HTML, which is invalid. We have + * a support issue open for it, and this plugin can potentially be removed when + * that's resolved. + */ +const remarkNestedList = () => { + const transform = node => { + if (node.type === 'list' && node.children && node.children.length > 1) { + node.children = node.children.reduce((acc, childNode, index) => { + if (index && childNode.children && childNode.children[0].type === 'list') { + acc[acc.length - 1].children.push(transform(childNode.children.shift())) + if (childNode.children.length) { + acc.push(transform(childNode)); + } + } else { + acc.push(transform(childNode)); + } + return acc; + }, []); + return node; + } + if (node.children) { + node.children = node.children.map(transform); + } + return node; + }; + return transform; +}; + +/** + * Dropbox Paper outputs emoji characters as images, and stores the actual + * emoji character in a `data-emoji-ch` attribute on the image. This plugin + * replaces the images with the emoji characters. + */ const rehypePaperEmoji = () => { const transform = node => { if (node.tagName === 'img' && node.properties.dataEmojiCh) { @@ -49,25 +90,29 @@ const rehypePaperEmoji = () => { }; export const markdownToHtml = markdown => { - console.log('markdownToHtml input', markdown); const result = unified() .use(markdownToRemark, remarkParseConfig) .use(remarkToRehype) + .use(rehypeRemoveEmpty) + .use(rehypeSanitize) + .use(rehypeMinifyWhitespace) .use(rehypeToHtml) .processSync(markdown) .contents; - console.log('markdownToHtml output', result); return result; } export const htmlToMarkdown = html => { - console.log('htmlToMarkdown input', html); const result = unified() .use(htmlToRehype, rehypeParseConfig) + .use(rehypePaperEmoji) + .use(rehypeSanitize) + .use(rehypeRemoveEmpty) + .use(rehypeMinifyWhitespace) .use(rehypeToRemark) + .use(remarkNestedList) .use(remarkToMarkdown, remarkStringifyConfig) .processSync(html) .contents; - console.log('htmlToMarkdown output', result); return result; };