2.0.2 • Published 7 years ago

peerio-interweave v2.0.2

Weekly downloads
1
License
MIT
Repository
github
Last release
7 years ago

Interweave v2.0.1

Build Status

Interweave is a robust React library that can...

  • Safely render HTML without using dangerouslySetInnerHTML.
  • Clean HTML attributes using filters.
  • Match and replace text using matchers.
  • Autolink URLs, IPs, emails, and hashtags.
  • Render Emoji characters.
  • And much more!

Requirements

  • React 0.14+
  • IE9+

Installation

Interweave requires React as a peer dependency.

npm install interweave react --save
// Or
yarn add interweave react

Usage

Interweave can primarily be used with the Interweave and Markup components, both of which provide an easy, straight-forward implementation for safely parsing and rendering HTML without using dangerouslySetInnerHTML (Facebook).

The Interweave component has the additional benefit of utilizing filters, matchers, and callbacks.

import Interweave from 'interweave';

<Interweave
  tagName="div"
  content="This string <b>contains</b> HTML."
/>

Props

  • content (string) - The string to render and apply matchers and filters to. Supports HTML.
  • emptyContent (node) - React node to render if no content was rendered.
  • tagName (div | span | p) - The HTML element tag name to wrap the output with. Defaults to "span".
  • filters (Filter[]) - Filters to apply to this instance.
  • matchers (Matcher[]) - Matchers to apply to this instance.
  • disableFilters (boolean) - Disables all filters.
  • disableMatchers (boolean) - Disables all matchers.
  • disableLineBreaks (boolean) - Disables automatic line break conversion.
  • noHtml (boolean) - Strips HTML tags from the content string while parsing.
  • onBeforeParse (func) - Callback that fires before parsing. Is passed the source string and must return a string.
  • onAfterParse (func) - Callback that fires after parsing. Is passed an array of strings/elements and must return an array.

Markup

Unlike the Interweave component, the Markup component does not support matchers, filters, or callbacks. This component is preferred when rendering strings that contain HTML is the primary use case.

import { Markup } from 'interweave';

<Markup content="This string <b>contains</b> HTML." />

Props

The Markup component only supports the content, emptyContent, tagName, disableLineBreaks, and noHtml props mentioned previously.

Documentation

Matchers

Matchers are the backbone of Interweave as they allow arbitrary insertion of React elements into strings, through the use of regex matching. This feature is quite powerful and opens up many possibilities.

It works by matching patterns within a string, deconstructing it into tokens, and reconstructing it back into an array of strings and React elements, therefore, permitting it to be rendered by React's virtual DOM layer. For example, take the following string "Check out my website, github.com/milesj!", and a UrlMatcher, you'd get the following array.

[
  'Check out my website, ',
  <Url>github.com/milesj</Url>,
  '!',
]

Matchers can be passed to each instance of Interweave. When adding a matcher, a unique name must be passed to the constructor.

import Interweave from 'interweave';
import EmojiMatcher from 'interweave/matchers/Emoji';

<Interweave matchers={[new EmojiMatcher('emoji')]} />

To disable all matchers per instance, pass the disableMatchers prop.

<Interweave disableMatchers />

To disable a single matcher, you can pass a prop that starts with "no", and ends with the unique name of the matcher (the one passed to the constructor). Using the example above, you can pass a noEmoji prop.

<Interweave noEmoji />

Creating A Matcher

To create a custom matcher, extend the base Matcher class, and define the following methods.

  • match(string) - Match the passed string using a regex pattern. This method must return null if no match is found, else it must return an object with a match key and the matched value. Furthermore, any additional keys defined in this object will be passed as props to the created element.
  • replaceWith(match, props) - Returns a React element that replaces the matched content in the string. The match is passed as the 1st argument, and any matched props or parent props are passed as the 2nd argument.
  • asTag() - The HTML tag name of the replacement element.
  • onBeforeParse (func) - Callback that fires before parsing. Is passed the source string and must return a string.
  • onAfterParse (func) - Callback that fires after parsing. Is passed an array of strings/elements and must return an array.
import { Matcher } from 'interweave';

export default class FooMatcher extends Matcher {
  match(string) {
    const matches = string.match(/foo/);

    if (!matches) {
      return null;
    }

    return {
      match: matches[0],
      extraProp: 'foo', // or matches[1], etc
    };
  }

  replaceWith(match, props) {
    const Tag = this.asTag();

    return (
      <Tag {...props}>{match}</Tag>
    );
  }

  asTag() {
    return 'span';
  }
}

To ease the matching process, there is a doMatch method that handles the null and object building logic. Simply pass it a regex pattern and a callback to build the object.

match(string) {
  return this.doMatch(string, /foo/, matches => ({
    extraProp: 'foo',
  }));
}

Rendered Elements

When a match is found, a React element is rendered (from a React component) from either the matcher's replaceWith method, or from a factory. What's a factory you ask? Simply put, it's a function passed to the constructor of a matcher, allowing the rendered element to be customized for built-in or third-party matchers.

To define a factory, simply pass a function to the 3rd argument of a matcher constructor. The factory function receives the same arguments as replaceWith.

new FooMatcher('foo', {}, (match, props) => (
  <span {...props}>{match}</span>
));

Elements returned from replaceWith or the factory must return an HTML element with the same tag name as defined by asTag.

Filters

Filters provide an easy way of cleaning HTML attribute values during the parsing cycle. This is especially useful for src and href attributes.

Filters can be added to each instance of Interweave. When adding a filter, the name of the attribute to clean must be passed as the first argument to the constructor.

import Interweave from 'interweave';

<Interweave filters={[new HrefFilter('href')]} />

To disable all filters, pass the disableFilters prop.

<Interweave disableFilters />

Creating A Filter

Creating a custom filter is easy. Simply extend the base Filter class and define a filter method. This method will receive the attribute value as the 1st argument, and it must return a string.

import { Filter } from 'interweave';

export default class SourceFilter extends Filter {
  filter(value) {
    return encodeURIComponent(value); // Clean attribute value
  }
}

Autolinking

Autolinking is the concept of matching patterns within a string and wrapping the matched result in a link (an <a> tag). This can be achieved with the matchers described below.

Note: The regex patterns in use for autolinking do not conform to the official RFC specifications, as we need to take into account word boundaries, punctuation, and more. Instead, the patterns will do their best to match against the majority of common use cases.

URLs, IPs

The UrlMatcher will match most variations of a URL and its segments. This includes the protocol, user and password auth, host, port, path, query, and fragment.

import Interweave from 'interweave';
import UrlMatcher from 'interweave/matchers/Url';

<Interweave matchers={[new UrlMatcher('url')]} />

The UrlMatcher does not support IP based hosts as I wanted a clear distinction between supporting these two patterns separately. To support IPs, use the IpMatcher, which will match hosts that conform to a valid IPv4 address (IPv6 not supported). Like the UrlMatcher, all segments are included.

import IpMatcher from 'interweave/matchers/Ip';

If a match is found, a Url element or matcher element will be rendered and passed the following props.

  • children (string) - The entire URL/IP that was matched.
  • urlParts (object)
    • scheme (string) - The protocol. Defaults to "http".
    • auth (string) - The username and password authorization, excluding @.
    • host (string) - The host, domain, or IP address.
    • port (number) - The port number.
    • path (string) - The path.
    • query (string) - The query string.
    • fragment (string) - The hash fragment, including #.

Emails

The EmailMatcher will match an email address and link it using a "mailto:" target.

import Interweave from 'interweave';
import EmailMatcher from 'interweave/matchers/Email';

<Interweave matchers={[new EmailMatcher('email')]} />

If a match is found, an Email element or matcher element will be rendered and passed the following props.

  • children (string) - The entire email address that was matched.
  • emailParts (object)
    • username (string) - The username. Found before the @.
    • host (string) - The host or domain. Found after the @.

Hashtags

The HashtagMatcher will match a common hashtag (like Twitter and Instagram) and link to it using a custom URL (passed as a prop). Hashtag matching supports alpha-numeric (a-z0-9), underscore (_), and dash (-) characters, and must start with a #.

Hashtags require a URL to link to, which is defined by the hashtagUrl prop. The URL must declare the following token, {{hashtag}}, which will be replaced by the matched hashtag.

import Interweave from 'interweave';
import HashtagMatcher from 'interweave/matchers/Hashtag';

<Interweave
  hashtagUrl="https://twitter.com/hashtag/{{hashtag}}"
  matchers={[new HashtagMatcher('hashtag')]}
/>

If a match is found, a Hashtag element or matcher element will be rendered and passed the following props.

  • children (string) - The entire hashtag that was matched.
  • hashtagName (string) - The hashtag name without #.

Render Emojis

Who loves emojis? Everyone loves emojis. Interweave has built-in support for rendering emoji, either their unicode character, or with media, all through the EmojiMatcher. The matcher utilizes EmojiOne for accurate and up-to-date data.

import Interweave from 'interweave';
import EmojiMatcher from 'interweave/matchers/Emoji';

<Interweave matchers={[new EmojiMatcher('emoji')]} />

Both unicode literal characters and escape sequences are supported when matching. If a match is found, an Emoji element or matcher element will be rendered and passed the following props.

  • shortName (string) - The shortname when convertShortName is on.
  • unicode (string) - The unicode literal character. Provided for both shortname and unicode matching.

Enlarged emoji

If content consists of only emojis, special .interweave__emoji--large class will be applied to each one of them.

Set enlargeUpTo emoji matcher option to disable enlarging for string containing more then enlargeUpTo emojis. Default value is 10. Set 0 to disable enlarging.

Converting Shortnames

Shortnames provide an easy non-unicode alternative for supporting emoji, and are represented by a word (or two) surrounded by two colons: :boy:. A list of all possible shortnames can be found at emoji.codes.

To enable conversion of a shortname to a unicode literal character, pass the convertShortName option to the matcher constructor.

new EmojiMatcher('emoji', { convertShortName: true });

Using SVGs or PNGs

To begin, we must enable conversion of unicode characters to media (images, vector, etc), by enabling the convertUnicode option. Secondly, if you want to support shortnames, enable convertShortName.

new EmojiMatcher('emoji', {
  convertShortName: true,
  convertUnicode: true,
});

Now we need to provide an absolute path to the SVG/PNG file using the emojiPath prop. This path must contain a {{hexcode}} token, which will be replaced by the hexadecimal value of the emoji.

<Interweave
  emojiPath="https://example.com/images/emoji/{{hexcode}}.png"
/>

Both media formats make use of the img tag, and will require an individual file, as sprites and icon fonts are not supported. The following resources can be used for downloading SVG/PNG icons.

Note: SVGs require CORS to work correctly, so files will need to be stored locally, or within a CDN under the same domain. Linking to remote SVGs will not work -- use PNGs instead.

CSS Styling

Since SVG/PNG emojis are rendered using the img tag, and dimensions can vary based on the size of the file, we must use CSS to restrict the size. The following styles work rather well, but the end result is up to you.

// Align in the middle of the text
.interweave__emoji {
  display: inline-block;
  vertical-align: middle;
}

// Match the size of the current text
.interweave__emoji img {
  width: 1em;
}

// Increase the size of large emoji
.interweave__emoji--large img {
  width: 2em;
}

Displaying Unicode Characters

To display native unicode characters as is, pass the renderUnicode option to the matcher constructor. This option will override the rendering of SVGs or PNGs, and works quite well alongside shortname conversion.

new EmojiMatcher('emoji', { renderUnicode: true });

HTML Parsing

Interweave doesn't rely on an HTML parser for rendering HTML safely, instead, it uses the DOM itself. It accomplishes this by using DOMImplementation.createHTMLDocument (MDN), which creates an HTML document in memory, allowing us to easily set markup, aggregate nodes, and generate React elements. This implementation is supported by all modern browsers and IE9+.

DOMImplementation has the added benefit of not requesting resources (images, scripts, etc) until the document has been rendered to the page. This provides an extra layer of security by avoiding possible CSRF and arbitrary code execution.

Furthermore, Interweave manages a whitelist of both HTML tags and attributes, further increasing security, and reducing the risk of XSS and vulnerabilities.

Tag Whitelist

Interweave keeps a mapping of valid HTML tags to parsing configurations. These configurations handle the following rules and processes.

  • Defines the type of rule: allow (render element and children), pass-through (ignore element and render children), deny (ignore both).
  • Defines the type of tag: inline, block, inline-block.
  • Flags whether inline children can be rendered.
  • Flags whether block children can be rendered.
  • Flags whether children of the same tag name can be rendered.
  • Maps the parent tags the current element can render in.
  • Maps the child tags the current element can render.

Lastly, any tag not found in the mapping will be flagged using the rule "deny", and promptly not rendered.

Attribute Whitelist

Interweave takes parsing a step further, by also filtering attribute values on all parsed HTML elements. Like tags, a mapping of valid HTML attributes to parser rules exist. A rule can be one of: allow and cast to string (default), allow and cast to number, allow and cast to boolean, and finally, deny.

Also like the tag whitelist, any attribute not found in the mapping is ignored.

Disabling HTML

The HTML parser cannot be disabled, however, a noHtml boolean prop can be passed to both the Interweave and Markup components. This prop will mark all HTML elements as pass-through, simply rendering text nodes recursively.

Global Configuration

In an older version of Interweave, there was this concept of global configuration, in which filters, matchers, and even nested props can be defined. This configuration would then be passed to all instances of Interweave or Markup. Since it was using globals, this approach had its fair share of problems.

In newer versions, we suggest composing around Interweave using a custom component in your application. This provides more options for customization, like the choice between Twitter and Instagram hashtags, or PNG or SVG emojis.

import React, { PropTypes } from 'react';
import BaseInterweave, { Filter, Matcher } from 'interweave';
import IpMatcher from 'interweave/matchers/Ip';
import UrlMatcher from 'interweave/matchers/Url';
import EmojiMatcher from 'interweave/matchers/Emoji';
import HashtagMatcher from 'interweave/matchers/Hashtag';

const globalFilters = [
  new CustomFilter('href'),
];

const globalMatchers = [
  new IpMatcher('ip'),
  new UrlMatcher('url'),
  new EmojiMatcher('emoji', { convertUnicode: true, convertShortName: true }),
  new HashtagMatcher('hashtag'),
];

export default function Interweave({
  filters = [],
  matchers = [],
  twitter = false,
  instagram = false,
  svg = false,
  ...props
}) {
  let emojiPath = '//cdnjs.cloudflare.com/ajax/libs/emojione/2.2.7/assets/png/{{hexcode}}.png';
  let hashtagUrl = '';

  if (svg) {
    emojiPath = emojiPath.replace('.png', '.svg');
  }

  if (twitter) {
    hashtagUrl = 'https://twitter.com/hashtag/{{hashtag}}';
  } else if (instagram) {
    hashtagUrl = 'https://instagram.com/explore/tags/{{hashtag}}';
  }

  return (
    <BaseInterweave
      filters={[
        ...globalFilters,
        ...filters,
      ]}
      matchers={[
        ...globalMatchers,
        ...matchers,
      ]}
      hashtagUrl={hashtagUrl}
      emojiPath={emojiPath}
      newWindow
      {...props}
    />
  )
}

Interweave.propTypes = {
  filters: PropTypes.arrayOf(PropTypes.instanceOf(Filter)),
  matchers: PropTypes.arrayOf(PropTypes.instanceOf(Matcher)),
  twitter: PropTypes.bool,
  instagram: PropTypes.bool,
  svg: PropTypes.bool,
};