Skip to content

Commit

Permalink
Merge pull request #24507 from code-dot-org/render-markdown-with-remark
Browse files Browse the repository at this point in the history
Render markdown with remark behind experiment flag
  • Loading branch information
Hamms committed Sep 17, 2018
2 parents 9c370f5 + c65d0cc commit 632b843
Show file tree
Hide file tree
Showing 7 changed files with 492 additions and 8 deletions.
1 change: 1 addition & 0 deletions apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@code-dot-org/maze": "1.1.0",
"@code-dot-org/p5.play": "1.3.0-cdo",
"@code-dot-org/piskel": "0.13.0-cdo.3",
"@code-dot-org/redactable-markdown": "0.3.3",
"@storybook/addon-info": "3.2.11",
"@storybook/addon-options": "^3.3.15",
"@storybook/react": "3.2.11",
Expand Down
33 changes: 28 additions & 5 deletions apps/src/templates/UnsafeRenderedMarkdown.jsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
import React, { PropTypes } from 'react';

import experiments from '@cdo/apps/util/experiments';

import processMarkdown from 'marked';
import renderer from "../util/StylelessRenderer";

import Parser from '@code-dot-org/redactable-markdown';

import expandableImages from './plugins/expandableImages';
import xmlAsTopLevelBlock from './plugins/xmlAsTopLevelBlock';
import stripStyles from './plugins/stripStyles';
import fixTightLists from './plugins/fixTightLists';

const remarkParser = Parser.create();

remarkParser.parser.use([
xmlAsTopLevelBlock,
expandableImages,
fixTightLists
]);

remarkParser.compilerPlugins.push(stripStyles);

/**
* Basic component for rendering a markdown string as HTML.
*
* Right now, it still uses marked; this will eventually be updated to use the
* new remark system, and possibly even support redaction.
*
* Note that this component will render anything contained in the markdown into
* the browser, including arbitrary html and script tags. It should be
* considered unsafe to use for user-generated content until the markdown
* renderer driving this component can be made safe.
*/
export default class UnsafeRenderedMarkdown extends React.Component {
static propTypes = {
markdown: PropTypes.string.isRequired
markdown: PropTypes.string.isRequired,
};

render() {
const processedMarkdown = processMarkdown(this.props.markdown, { renderer });
let processedMarkdown;
if (experiments.isEnabled('remark')) {
processedMarkdown = remarkParser.sourceToHtml(this.props.markdown);
} else {
processedMarkdown = processMarkdown(this.props.markdown, { renderer });
}

/* eslint-disable react/no-danger */
return (
<div
Expand Down
34 changes: 34 additions & 0 deletions apps/src/templates/plugins/expandableImages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* "Expandable Images" are simply images we want to include as thumbnails, which
* will pop out into a modal when clicked on. The trigger for an image to be
* treated as expandable is if the image's alt text ends with the string
* "expandable".
*
* See https://github.com/code-dot-org/code-dot-org/pull/16676 for original
* implementation.
*/
module.exports = function expandableImages() {
const Parser = this.Parser;
const tokenizers = Parser.prototype.inlineTokenizers;
const originalImage = tokenizers.link;
tokenizers.link = function (eat, value, silent) {
const link = originalImage.call(this, eat, value, silent);
if (link && link.type === "image" && link.alt && link.alt.endsWith("expandable")) {
link.type = "span";
link.data = {
hName: 'span',
hProperties: {
dataUrl: link.url,
className: "expandable-image"
}
};
link.children = [{
type: 'text',
value: link.alt.substr(0, -1 * "expandable".length).trim()
}];
}

return link;
};
tokenizers.link.locator = originalImage.locator;
};
45 changes: 45 additions & 0 deletions apps/src/templates/plugins/fixTightLists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* This is a terrible, horrible, no good, very bad hack.
*
* In short, markdown lists can be either "loose", meaning with list items
* separated by extra newlines, or "tight". Most (all?) of the lists in our
* level content are tight.
*
* In nearly all markdown implementations, tight lists do NOT wrap their
* textual content in paragraph tags, and loose lists do. Unfortunately, in
* remark, all list content is wrapped in paragraph tags. Until that can be
* fixed, we apply a post-tokenization cleanup step to go through and
* conditionally remove paragraph tags that should not have been created in
* the first place.
*
* Possible fixes are in-discussion at:
*
* https://github.com/syntax-tree/mdast-util-to-hast/pull/23
* https://github.com/remarkjs/remark/pull/349
*/
module.exports = function fixTightLists() {
const Parser = this.Parser;
const tokenizers = Parser.prototype.blockTokenizers;
const originalList = tokenizers.list;
tokenizers.list = function (eat, value, silent) {
const result = originalList.call(this, eat, value, silent);

if (result && !result.loose && result.children) {
result.children.forEach(function (listItem) {
if (listItem.children) {
listItem.children = listItem.children.reduce(function (children, child) {
if (child.type === "paragraph") {
children = children.concat(child.children);
} else {
children.push(child);
}
return children;
}, []);
}
});
}

return result;
};
};

17 changes: 17 additions & 0 deletions apps/src/templates/plugins/stripStyles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Refuse to render style tags.
*
* Note that once we settle on a final compiler, we should be able to actually
* write this as a plugin that modifies the way the compiler works (rather than
* one that just sits on the "out" pipe of the compiler and monitors results) in
* order to really efficiently support rendering just a strict whitelist of HTML
* tags.
*/
module.exports = function stripStyles() {
const visitors = this.Compiler.prototype.visitors;
const originalHtml = visitors.html;
visitors.html = function (node, parent) {
const originalResult = originalHtml.call(this, node, parent);
return originalResult.indexOf('<style>') !== -1 ? '' : originalResult;
};
};
14 changes: 14 additions & 0 deletions apps/src/templates/plugins/xmlAsTopLevelBlock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* By default, and as per markdown standards, redactable markdown will not treat
* XML blocks as top-level blocks, but will instead wrap all of them in
* paragraph tags if they are not already in a paragraph.
*
* Unfortunately, we actually leverage that property of marked to allow us to
* visually differentiate between inline and regular embedded Blockly blocks:
* https://github.com/code-dot-org/code-dot-org/blob/9aed16a2e6b8aeaf3c97e6959f3ec62c61356024/apps/src/templates/instructions/utils.js#L76
*
* If we change how that's done, we can take away this plugin.
*/
module.exports = function xmlAsTopLevelBlock() {
this.Parser.prototype.options.blocks.push('xml');
};

0 comments on commit 632b843

Please sign in to comment.