Add custom React-like components to Markdown which can be safely used by end-users. Use with your favorite Markdown engine.
E.g.,
<# A Box which defaults to blue if user has no favorite color #>
<Box color={user.favoriteColor or "blue"} lineWidth=3>
## subheading
* listElement1
* listElement2
[google](https://google.com)
<Box color="red">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 interpolated expressions:
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 interpolated 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, expressions, like { a.b }
or { foo(a) }
are restricted to a context object and a set of developer defined functions, so there is no script injection vulnerability. Authors of this markdown work inside a developer-defined sandbox.
Easy one step method for generating HTML.
Parses and renders Markdown with components to HTML.
// 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" d e=false >
# User likes { user.color or "no" } color
</MyComponent>
`);
// =>
// [
// {
// type: "tag",
// name: 'mycomponent',
// rawName: 'MyComponent',
// attribs: {
// a: {
// type: "interpolation",
// expression: ["accessor", "x.y.z"]
// },
// b: 123,
// c: "hello",
// d: true,
// e: false
// }
// children: [
// {
// type: "text",
// blocks: [
// "<h1>User likes ",
// { type: "interpolation",
// expression: ["or", ["accessor", "user.color"], ["scalar", "no"]]
// },
// "color</h1>"
// ]
// }
// ]
// }
// ]
Attributes can be ints, floats, strings, booleans and expressions.
<MyComponent a=1 b=1.2 c="hello" d e=true f=false />
Note: the d
attribute represents a true
boolean.
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. -
functions
(optional) Functions which may be used in interpolation expressions, of the form:(context, args) => value
Writes an element (e.g., the result from Parser.parse) to stream
, and uses the context
when evaluating expressions:
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>
Interpolation blocks can contain simple expressions including function calls:
<Component value={ not (foo(true)) and add(123, -123) or x.y } />
Interpolation functions are provided to the renderer constructor:
new Renderer({
components: {
Component: (context, renderer) => {...}
},
functions: {
foo(context, myBool) { return myBool; },
add(context, a, b) { return a+b; }
}
});
Given the above code, the value
attribute of Component
will be the value of x.y:
value={ not (true) and 0 or x.y }
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>