@readme/markdown
Version:
ReadMe's React-based Markdown parser
216 lines (187 loc) • 6.97 kB
JavaScript
// There's a bug in jsdom where Jest spits out heaps of errors from it not being able to interpret
// this file, so let's not include this when running tests since we aren't doing visual testing
// anyways.
// https://github.com/jsdom/jsdom/issues/217
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line global-require
require('./styles/main.scss');
}
const React = require('react');
const unified = require('unified');
/* Unified Plugins
*/
const sanitize = require('hast-util-sanitize/lib/github.json');
// remark plugins
const remarkRehype = require('remark-rehype');
const rehypeRaw = require('rehype-raw');
const remarkParse = require('remark-parse');
const remarkStringify = require('remark-stringify');
const remarkBreaks = require('remark-breaks');
// rehype plugins
const rehypeSanitize = require('rehype-sanitize');
const rehypeStringify = require('rehype-stringify');
const rehypeReact = require('rehype-react');
/* React Custom Components
*/
const Variable = require('@readme/variable');
const GlossaryItem = require('./components/GlossaryItem');
const Code = require('./components/Code');
const Table = require('./components/Table');
const Anchor = require('./components/Anchor');
const Heading = require('./components/Heading');
const Callout = require('./components/Callout');
const CodeTabs = require('./components/CodeTabs');
const Image = require('./components/Image');
const Embed = require('./components/Embed');
/* Custom Unified Parsers
*/
const flavorCodeTabs = require('./processor/parse/flavored/code-tabs');
const flavorCallout = require('./processor/parse/flavored/callout');
const flavorEmbed = require('./processor/parse/flavored/embed');
const magicBlockParser = require('./processor/parse/magic-block-parser');
const variableParser = require('./processor/parse/variable-parser');
const gemojiParser = require('./processor/parse/gemoji-parser');
/* Custom Unified Compilers
*/
const rdmeDivCompiler = require('./processor/compile/div');
const codeTabsCompiler = require('./processor/compile/code-tabs');
const rdmeEmbedCompiler = require('./processor/compile/embed');
const rdmeVarCompiler = require('./processor/compile/var');
const rdmeCalloutCompiler = require('./processor/compile/callout');
const rdmePinCompiler = require('./processor/compile/pin');
// Processor Option Defaults
const options = require('./processor/options.json');
// Sanitization Schema Defaults
sanitize.clobberPrefix = '';
sanitize.tagNames.push('rdme-pin');
sanitize.tagNames.push('embed');
sanitize.attributes.embed = ['url', 'provider', 'html', 'title', 'href'];
sanitize.tagNames.push('rdme-embed');
sanitize.attributes['rdme-embed'] = ['url', 'provider', 'html', 'title', 'href'];
sanitize.attributes.a = ['href', 'title'];
sanitize.tagNames.push('figure');
sanitize.tagNames.push('figcaption');
sanitize.tagNames.push('input'); // allow GitHub-style todo lists
sanitize.ancestors.input = ['li'];
/**
* Normalize Magic Block Raw Text
*/
export function normalize(blocks) {
// normalize magic block lines
// eslint-disable-next-line no-param-reassign
blocks = blocks
.replace(/\[block:/g, '\n[block:')
.replace(/\[\/block\]/g, '[/block]\n')
.trim()
.replace(/^(#+)(.+)/gm, '$1 $2');
return `${blocks}\n\n `;
}
export const utils = {
options,
normalizeMagic: normalize,
VariablesContext: Variable.VariablesContext,
GlossaryContext: GlossaryItem.GlossaryContext,
};
/**
* Core markdown text processor
*/
function parseMarkdown(opts = {}) {
/*
* This is kinda complicated: "markdown" within ReadMe is
* often more than just markdown. It can also include HTML,
* as well as custom syntax constructs such as <<variables>>,
* and other special features.
*
* We use the Unified text processor to parse and transform
* Markdown to various output formats, such as a React component
* tree. (See https://github.com/unifiedjs/unified for more.)
*
* The order for processing ReadMe-flavored markdown is as follows:
* - parse markdown
* - parse custom syntaxes add-ons using custom compilers
* - convert from a remark mdast (markdown ast) to a rehype hast (hypertext ast)
* - extract any raw HTML elements
* - sanitize and remove any disallowed attributes
* - output the hast to a React vdom with our custom components
*/
return unified()
.use(remarkParse, opts.markdownOptions)
.data('settings', opts.settings)
.use(magicBlockParser.sanitize(sanitize))
.use([flavorCodeTabs.sanitize(sanitize), flavorCallout.sanitize(sanitize), flavorEmbed.sanitize(sanitize)])
.use(variableParser.sanitize(sanitize))
.use(!opts.correctnewlines ? remarkBreaks : () => {})
.use(gemojiParser.sanitize(sanitize))
.use(remarkRehype, { allowDangerousHTML: true })
.use(rehypeRaw)
.use(rehypeSanitize, sanitize);
}
export function plain(text, opts = options) {
if (!text) return null;
return parseMarkdown(opts)
.use(rehypeReact, {
createElement: React.createElement,
Fragment: React.Fragment,
})
.processSync(text).contents;
}
/**
* return a React VDOM component tree
*/
export function react(text, opts = options) {
if (!text) return null;
// eslint-disable-next-line react/prop-types
const PinWrap = ({ children }) => <div className="pin">{children}</div>;
const count = {};
return parseMarkdown(opts)
.use(rehypeReact, {
createElement: React.createElement,
Fragment: React.Fragment,
components: {
'code-tabs': CodeTabs(sanitize),
'rdme-callout': Callout(sanitize),
'readme-variable': Variable,
'readme-glossary-item': GlossaryItem,
'rdme-embed': Embed(sanitize),
'rdme-pin': PinWrap,
table: Table(sanitize),
a: Anchor(sanitize),
h1: Heading(1, count),
h2: Heading(2, count),
h3: Heading(3, count),
h4: Heading(4, count),
h5: Heading(5, count),
h6: Heading(6, count),
code: Code(sanitize),
img: Image(sanitize),
},
})
.processSync(text).contents;
}
/**
* transform markdown in to HTML
*/
export function html(text, opts = options) {
if (!text) return null;
return parseMarkdown(opts).use(rehypeStringify).processSync(text).contents;
}
/**
* convert markdown to an mdast object
*/
export function ast(text, opts = options) {
if (!text) return null;
return parseMarkdown(opts).use(remarkStringify, opts.markdownOptions).parse(text);
}
/**
* compile mdast to ReadMe-flavored markdown
*/
export function md(tree, opts = options) {
if (!tree) return null;
return parseMarkdown(opts)
.use(remarkStringify, opts.markdownOptions)
.use([rdmeDivCompiler, codeTabsCompiler, rdmeCalloutCompiler, rdmeEmbedCompiler, rdmeVarCompiler, rdmePinCompiler])
.stringify(tree);
}
const ReadMeMarkdown = text => react(normalize(text));
export default ReadMeMarkdown;