From 83b71a4f49cbea49a0e06212a9d6e84afe0f9f0f Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 28 Apr 2020 12:41:03 +1200 Subject: [PATCH] Shift markdown parsing into helper function Slight changes in function in that table elements are now allowed in the footer as well as the main-narrative-markdown. This was commented as a "to-do" in the code --- src/components/framework/footer.js | 32 +-------------- .../narrative/MainDisplayMarkdown.js | 39 +------------------ src/util/parseMarkdown.js | 33 ++++++++++++++++ 3 files changed, 37 insertions(+), 67 deletions(-) create mode 100644 src/util/parseMarkdown.js diff --git a/src/components/framework/footer.js b/src/components/framework/footer.js index d2f29aaac..26a5cd2ae 100644 --- a/src/components/framework/footer.js +++ b/src/components/framework/footer.js @@ -1,7 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import marked from "marked"; -import dompurify from "dompurify"; import styled from 'styled-components'; import { withTranslation } from "react-i18next"; import { FaDownload } from "react-icons/fa"; @@ -13,6 +11,7 @@ import { version } from "../../version"; import { publications } from "../download/downloadModal"; import { isValueValid } from "../../util/globals"; import hardCodedFooters from "./footer-descriptions"; +import { parseMarkdown } from "../../util/parseMarkdown"; const dot = ( @@ -138,36 +137,9 @@ export const getAcknowledgments = (metadata, dispatch) => { * Jover. December 2019. */ if (metadata.description) { - dompurify.addHook("afterSanitizeAttributes", (node) => { - // Set external links to open in a new tab - if ('href' in node && location.hostname !== node.hostname) { - node.setAttribute('target', '_blank'); - node.setAttribute('rel', 'noreferrer nofollow'); - } - // Find nodes that contain images and add imageContainer class to update styling - const nodeContainsImg = ([...node.childNodes].filter((child) => child.localName === 'img')).length > 0; - if (nodeContainsImg) { - // For special case of image links, set imageContainer on outer parent - if (node.localName === 'a') { - node.parentNode.className += ' imageContainer'; - } else { - node.className += ' imageContainer'; - } - } - }); - - const sanitizer = dompurify.sanitize; - const sanitizerConfig = { - ALLOWED_TAGS: ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img', '#text', 'code', 'pre', 'hr'], - ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'alt'], - KEEP_CONTENT: false, - ALLOW_DATA_ATTR: false - }; - let cleanDescription; try { - const rawDescription = marked(metadata.description); - cleanDescription = sanitizer(rawDescription, sanitizerConfig); + cleanDescription = parseMarkdown(metadata.description); } catch (error) { console.error(`Error parsing footer description: ${error}`); cleanDescription = '

There was an error parsing the footer description.

'; diff --git a/src/components/narrative/MainDisplayMarkdown.js b/src/components/narrative/MainDisplayMarkdown.js index 49a89e3ef..15750ace6 100644 --- a/src/components/narrative/MainDisplayMarkdown.js +++ b/src/components/narrative/MainDisplayMarkdown.js @@ -1,10 +1,8 @@ import React from "react"; import { connect } from "react-redux"; -import marked from "marked"; import styled from 'styled-components'; -import dompurify from "dompurify"; import { dataFont } from "../../globalStyles"; - +import { parseMarkdown } from "../../util/parseMarkdown"; /** * The following code borrows heavily from the Footer @@ -105,7 +103,7 @@ const Container = styled.div` `; const EXPERIMENTAL_MainDisplayMarkdown = ({narrativeBlock, width, mobile}) => { - const cleanHTML = mdToHtml(narrativeBlock.mainDisplayMarkdown); + const cleanHTML = parseMarkdown(narrativeBlock.mainDisplayMarkdown); return (
{ export default connect((state) => ({ narrativeBlock: state.narrative.blocks[state.narrative.blockIdx] }))(EXPERIMENTAL_MainDisplayMarkdown); - -function mdToHtml(md) { - /* this is copy & pasted from `../framework/footer.js` and should be abstracted - into a function */ - dompurify.addHook("afterSanitizeAttributes", (node) => { - // Set external links to open in a new tab - if ('href' in node && location.hostname !== node.hostname) { - node.setAttribute('target', '_blank'); - node.setAttribute('rel', 'noreferrer nofollow'); - } - // Find nodes that contain images and add imageContainer class to update styling - const nodeContainsImg = ([...node.childNodes].filter((child) => child.localName === 'img')).length > 0; - if (nodeContainsImg) { - // For special case of image links, set imageContainer on outer parent - if (node.localName === 'a') { - node.parentNode.className += ' imageContainer'; - } else { - node.className += ' imageContainer'; - } - } - }); - - const sanitizer = dompurify.sanitize; - const sanitizerConfig = { - ALLOWED_TAGS: ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img', '#text', 'pre', 'hr', 'table', 'thead', 'tbody', 'th', 'tr', 'td'], - ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'alt'], - KEEP_CONTENT: false, - ALLOW_DATA_ATTR: false - }; - const rawDescription = marked(md); - const cleanDescription = sanitizer(rawDescription, sanitizerConfig); - return cleanDescription; -} diff --git a/src/util/parseMarkdown.js b/src/util/parseMarkdown.js new file mode 100644 index 000000000..7624d757a --- /dev/null +++ b/src/util/parseMarkdown.js @@ -0,0 +1,33 @@ +import marked from "marked"; +import dompurify from "dompurify"; + +dompurify.addHook("afterSanitizeAttributes", (node) => { + // Set external links to open in a new tab + if ('href' in node && location.hostname !== node.hostname) { + node.setAttribute('target', '_blank'); + node.setAttribute('rel', 'noreferrer nofollow'); + } + // Find nodes that contain images and add imageContainer class to update styling + const nodeContainsImg = ([...node.childNodes].filter((child) => child.localName === 'img')).length > 0; + if (nodeContainsImg) { + // For special case of image links, set imageContainer on outer parent + if (node.localName === 'a') { + node.parentNode.className += ' imageContainer'; + } else { + node.className += ' imageContainer'; + } + } +}); + +const ALLOWED_TAGS = ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img']; +ALLOWED_TAGS.push('#text', 'code', 'pre', 'hr', 'table', 'thead', 'tbody', 'th', 'tr', 'td'); + +const ALLOWED_ATTR = ['href', 'src', 'width', 'height', 'alt']; + +export const parseMarkdown = (mdString) => { + const sanitizer = dompurify.sanitize; + const sanitizerConfig = {ALLOWED_TAGS, ALLOWED_ATTR, KEEP_CONTENT: false, ALLOW_DATA_ATTR: false}; + const rawDescription = marked(mdString); + const cleanDescription = sanitizer(rawDescription, sanitizerConfig); + return cleanDescription; +};