Add custom React-like components to Markdown which can be safely used by end-users. Use with your favorite Markdown engine.
E.g.,
<Box color={user.favoriteColor} lineWidth=3>
## subheading
* listElement1
* listElement2
[google](https://google.com)
<Box color=blue>Box in box!</Box>
_more_ markdown
</Box>
npm i markdown-components
// plus your favorite markdown engine
// npm i markdown
// npm i showdown
// npm i markdown-it
var { toHTML, markdownItEngine } = require("markdown-components");
// define a Box component:
var components = {
Box: function({ lineSize, color, __children }, render) {
render(
`<div style="border-width:${lineSize}; background-color:${color};">`
);
render(__children); // render internal elements
render(`</div>`);
}
};
// use the Box component:
var customizedMarkdown = `
Custom components:
<Box lineSize=2 color={ user.favoriteColor }>
Can contain...
# Markdown with interpolation:
This box should be *{ user.favoriteColor }*
And the _markdown_ can contain custom components:
<Box lineSize=1 color="red">
which can contain *more markdown*
and so on.
Render open curly brace and open angle bracket: {{ and <<
</Box>
</Box>`;
// render the markdown with your custom components,
// providing context variables:
var html = toHTML({
input: customizedMarkdown,
components: components,
context: { user: { favoriteColor: 'blue' }},
markdownEngine: markdownItEngine()
});
console.log(html); // ~=>
// <p>Custom components:</p>
// <div style="border-width:2; background-color>
// <p>Can contain...</p>
// <h1> Markdown with interpolation:</h1>
// <p>This box should be <b>blue</b>
// And the <i>markdown</i> can contain custom components:</p>
// <div style="border-width:1; background-color:red>
// <p>which can contain <b>more markdown</b>
// and so on.
// Render open curly brace and open angle bracket: { and <</p>
// </div>
// </div>
The components argument to toHTML
and Renderer.write
provides functions that generate HTML.
For example:
{
Box: function ({__name, __children, color}, render) {
// generate custom HTML:
render(`<div class="box" style="background-color:${color}">`);
render(__children); // render elements between start and end tag
render(`</div>`);
}
}
Allows you to write:
<Box color="red">
# This markdown
Will be displayed on a red background
</Box>
Markdown components provides a content authoring language with custom components which is safe for use by end-users.
JSX-Markdown | markdown-it-shortcodes | markdown-components | |
---|---|---|---|
end-users | unsafe | safe | safe |
nesting | yes | no | yes |
HOCs | yes | no | yes |
JSX-markdown libraries aren't suitable because React interpolation expressions are Javascript. I.e., you'd need to eval user-generated javascript either on your server or another user's browser. You could try evaluating such code in a sandboxed environment, but it's inefficient and asynchronous. The need for asynchronous evaluation rules out using a sandbox like jailed in a React client, since React rendering requires synchronous execution.
In this package, interpolation expressions, like { a.b }
, are not evaluated, so there is no script injection vulnerability, and inteprolation is a simple synchronous function. End-users only have access to variables you provide in a context object.
Easy one step method for generating HTML.
Parses and renders Markdown with components to HTML, interpolating context variables.
// requires: npm install markdown-it
import { markdownItEngine, toHTML } from 'markdown-components';
toHTML({
input: '<MyComponent a={ x.y } b=123 c="hello"># This is an {x.y} heading</MyComponent>',
components: {
MyComponent({a, b, c, __children}, render) {
render(`<div class=my-component><p>a=${a};b=${b};c=${c}</p>`);
render(__children); // renders elements between open and close tag
render(`</div>`);
}
},
markdownEngine: markdownItEngine(),
context:{ x: { y: "interpolated" } }
// defaultComponent,
// interpolator
});
// =>
// "<div class=my-component><p>a=interpolated;b=123;c=hello</p><h1>This is an interpolated heading</h1></div>"
Class for parsing component markdown input text.
Note that this function doesn't parse Markdown. Markdown parsing is currently done by the renderer. This is expected to change in future.
markdownEngine
(required) The markdown engine function (required).indentedMarkdown
(optional, default: true) Allows a contiguous block of Markdown to start at an indentation point without creating a preformatted code block. This is useful when writing Markdown inside deeply nested components.
Returns a JSON object representing the parsed markdown.
import { Parser, showdownEngine } from 'markdown-components';
var parser = new Parser({markdownEngine:}); // use showdownjs
var parsedElements = parser.parse(`<MyComponent a={ x.y.z } b=123 c="hello">
# User likes { user.color } color
</MyComponent>
`);
// =>
// [
// {
// type: "tag",
// name: 'mycomponent',
// rawName: 'MyComponent',
// attribs: {
// a: { accessor: "x.y.z" },
// b: 123,
// c: "hello"
// }
// children: [
// {
// type: "text",
// blocks: [
// "<h1>User likes ",
// { type: "interpolation", accessor: "user.color" }
// "color</h1>"
// ]
// }
// ]
// }
// ]
A class representing the rendering logic.
components
(required) An object of key:function pairs. Where the key is the componentName (matched case-insensitively with tags in the input text), and function is a function which takes parsed elements as input, and uses the render function to write HTML.({__name, __children, ...attrs}, render)=>{}
defaultComponent
(optional) A function called when a matching component cannot be found for a tag. Same function signature as a component.interpolator
(optional) Takes the context provided to#write
ortoHTML
and the string inside a{ }
block, and returns a new value. The default interpolator provides a simple dot-accessor syntax for objects.standardInterpolator({a: {b: 123}}, "a.b") // => 123
Writes an element (e.g., the result from Parser.parse) to stream
, interpolating variables from the context
:
renderer.write(elements, context, stream);
var html = stream.toString();
The components argument is an object where keys are tag names, and functions render HTML. This is a required argument of the Renderer
constructor and the toHTML
function.
For example:
{
Box: function ({__name, __children, color}, render) {
// generate custom HTML:
render(`<div class="box" style="background-color:${color}">`);
render(__children); // render elements between start and end tag
render(`</div>`);
}
}
Allows you to write:
<Box color="red">
# This markdown
Will be displayed on a red background
</Box>
Component functions are of the form:
(tagArguments, render) => { }
The first argument, tagArguments, contains values passed in the markup, plus two special keys:
__name
name of the tag
__children
array of Objects representing elements between the open and close tags, having the form:
The second argument, render
is a function which takes a string representing HTML or an object representing parsed entities and writes it to a stream.
Because the component has responsibility for rendering __children
, you can manipulate child elements at render time, choosing to ignore, rewrite or reorder them. For example, you could create elements that provide switch/case/default semantics:
# Your Results
<Switch value={user.score}>
<Case value="A">You did _great_!</Case>
<Case value="B">Well done</Case>
<Default>Better luck next time</Default>
</Switch>
An optional function which returns a value given the context and an accessor expression (the value contained between the braces in #{...}
):
The default interpolator has behavior similar to lodash's get. It safely traverses object using a dot-separated accessor.
For example, given a context of { a: {b: 9 }}
, { a.b }
provides an interpolated value of 9
, and { x.y.z }
is undefined
.
function myInterpolator(context, accessor) {
return context[accessor];
}
toHTML({
interpolator: myInterpolator,
...
});
A number of wrappers for existing Markdown interpreters are provided in src/engines.js
. Each is a function which returns a rendering function. There are wrappers MarkdownIt, ShowdownJS and evilStreak's markdown. It's easy to write your own wrapper. See the source file.
import { toHTML, markdownItEngine } from 'markdown-components';
var html = toHTML({
markdownEngine: markdownItEngine,
...
});
If you're concerned about efficiency, parse the input first, and cache the result (a plain JSON object). Call Renderer.write with different contexts:
var { markdownItEngine, Renderer, Parser } = require('markdown-components'); // "npm i markdown-it" to use markdownItEngine
var streams = require('memory-streams'); // "npm i memory-streams"
var renderer = new Renderer({
componets: {
Box({ __children, color }, render) {
render(`<div class="box" style="background-color:${color}">`);
render(__children);
render(`</div>`);
}
}
});
var parser = new Parser({ markdownEngine: markdownItEnginer() });
var parsedElements = parser.parse('<Box color={user.favoriteColor}>_Here is some_ *markdown*</Box>');
// red box
stream = streams.getWriteableStream();
renderer.write(parsedElements,{ user: { favoriteColor: "red" } }, stream);
console.log(stream.toString());
// <div class="box" style="background-color:red"><i>Here is some</i> <b>markdown</b></div>
// blue box
stream = streams.getWriteableStream();
renderer.write(parsedElements,{ user: { favoriteColor: "blue" } }, stream);
console.log(stream.toString());
// <div class="box" style="background-color:blue"><i>Here is some</i> <b>markdown</b></div>