Coder Social home page Coder Social logo

smart-report's Introduction

CircleCI Maintainability Test Coverage

smart-report

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>
  <# this is a comment #>
  ## subheading
  * listElement1
  * listElement2
  [google](https://google.com)
  <Box color="red">Box in box!</Box>
  _more_ markdown
</Box>

Install

yarn add git@github.com:precisely/smart-report.git
// plus your favorite markdown engine
// npm i markdown
// npm i showdown
// npm i markdown-it

Quick start

var { toHTML, markdownItEngine } = require("smart-report");

// 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, errors} = 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 &lt</p>
// </div>
// </div>

Custom Components

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>

Errors

The toHTML and Parser.parse methods return a key, errors containing a list of errors detected. For example:

const {elements, errors} = parser.parse('#Oh oh\n<Foo>');
console.log(errors); // =>
// [{type: 'NoCloseTag', location: { lineNumber: 2, columnNumber: 21 }, message: 'Expecting closing tag </Foo>'}]

Rationale

Markdown components provides a content authoring language with custom components which is safe for use by end-users.

JSX-Markdown markdown-it-shortcodes smart-report
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.

API

toHTML

Easy one step method for generating HTML.

Parses and renders Markdown with components to HTML.

// requires: npm install markdown-it
import { markdownItEngine, toHTML } from 'smart-report';
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>"

Parser

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.

constructor arguments

  • 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.

#parse

Returns a JSON object representing the parsed markdown.

import { Parser, showdownEngine } from 'smart-report';
var parser = new Parser({markdownEngine:}); // use showdownjs
var {elements, errors} = parser.parse(`<MyComponent a={ x.y.z } b=123 c="hello" d e=false >
# User likes { user.color or "no" } color
</MyComponent>
`);
// =>
// { elements: [
//   {
//     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>"
//         ]
//       }
//     ]
//   }
// ], errors: []
// }

Attribute types

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.

Renderer

A class representing the rendering logic.

constructor arguments

  • 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

#write

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();

Renderer Components

The components argument is an object where keys are tag names, and functions render HTML. This is a required argument of the Renderer constructors 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.

Higher Order Components

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 Functions

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 }

Markdown Engine

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 'smart-report';

var html = toHTML({
  markdownEngine: markdownItEngine,
  ...
});

Separately Parse and Render

If you're concerned about efficiency, parse the input first, and cache the result (a plain JSON object). Call Renderer.write with different contexts:

Example

var { markdownItEngine, Renderer, Parser } = require('smart-report'); // "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 {elements, errors} = parser.parse('<Box color={user.favoriteColor}>_Here is some_ *markdown*</Box>');

// red box
stream = streams.getWriteableStream();
renderer.write(elements,{ 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(elements,{ user: { favoriteColor: "blue" } }, stream);
console.log(stream.toString());
// <div class="box" style="background-color:blue"><i>Here is some</i> <b>markdown</b></div>

Reducer

The Reducer class reduces a parse tree to a minimal context-independent form. The Reducer.reduce function takes a parse tree and a context, and returns a minimized parse tree which can be rendered without a context. This is useful, for personalizing and removing other parts of a smart report on the server before sending it to a client for rendering.

constructor

const reducer = new Reducer({
  components: {
    Foo(elt, context) {
      // the <Foo> tag selects children which have attr foo==true
      const reducedChildren = elt.children.filter(child => child.attrs.foo);
      return [reducedChildren, context]; // the context is returned unchanged
    }
  }
})

#reduce

const parser = new Parser();
const {elements} = parser.parse('some text');
const context = {};
reducer.reduce(elements, context);

smart-report's People

Contributors

aneilbaboo avatar efueger avatar visheshd avatar

smart-report's Issues

Tag context is not passed to interpolated attributes during reduction

This needs to be fixed. The reducer currently reduces all attributes with the top-level context, then reduces tags. However, this means that the context generated by tag reducers is not passed to attributes, which could cause a problem.

The solution is to reduce attributes incrementally during tag reduction, even for children which are removed by the tag reducer.

One solution might be to revert to ea5fcbe8b95d7c1cb469a300fe1571b15de652e2 and fix it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.