All files / src Matcher.js

100% Statements 22/22
94.74% Branches 18/19
100% Functions 8/8
100% Lines 22/22
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104                                      471x 1x     470x 470x 470x 470x               3199x   3199x 2x   3197x     3199x 1x     3198x             1x             1x                 1x               5594x   5594x 2435x     3159x                   19x             13x      
/**
 * @copyright   2016, Miles Johnson
 * @license     https://opensource.org/licenses/MIT
 * @flow
 */
 
import React from 'react';
 
import type { MatcherFactory, MatchResponse, ParsedNodes } from './types';
 
type MatchCallback = (matches: string[]) => ({ [key: string]: string | Object });
 
export default class Matcher<T> {
  options: T;
  propName: string;
  inverseName: string;
  factory: ?MatcherFactory;
 
  constructor(name: string, options: T, factory: ?MatcherFactory = null) {
    if (!name || name.toLowerCase() === 'html') {
      throw new Error(`The matcher name "${name}" is not allowed.`);
    }
 
    this.options = options || {};
    this.propName = name;
    this.inverseName = `no${name.charAt(0).toUpperCase() + name.substr(1)}`;
    this.factory = factory;
  }
 
  /**
   * Attempts to create a React element using a custom user provided factory,
   * or the default matcher factory.
   */
  createElement(match: string, props: Object = {}): React.Element<*> {
    let element = null;
 
    if (typeof this.factory === 'function') {
      element = this.factory(match, props);
    } else {
      element = this.replaceWith(match, props);
    }
 
    if (typeof element !== 'string' && !React.isValidElement(element)) {
      throw new Error(`Invalid React element created from ${this.constructor.name}.`);
    }
 
    return element;
  }
 
  /**
   * Replace the match with a React element based on the matched token and optional props.
   */
  replaceWith(match: string, props: Object = {}): React.Element<*> {
    throw new Error(`${this.constructor.name} must return a React element.`);
  }
 
  /**
   * Defines the HTML tag name that the resulting React element will be.
   */
  asTag(): string {
    throw new Error(`${this.constructor.name} must define the HTML tag name it will render.`);
  }
 
  /**
   * Attempt to match against the defined string.
   * Return `null` if no match found, else return the `match`
   * and any optional props to pass along.
   */
  match(string: string): ?MatchResponse {
    throw new Error(`${this.constructor.name} must define a pattern matcher.`);
  }
 
  /**
   * Trigger the actual pattern match and package the matched
   * response through a callback.
   */
  doMatch(string: string, pattern: string | RegExp, callback: MatchCallback): ?MatchResponse {
    const matches = string.match((pattern instanceof RegExp) ? pattern : new RegExp(pattern, 'i'));
 
    if (!matches) {
      return null;
    }
 
    return {
      ...callback(matches),
      match: matches[0],
    };
  }
 
  /**
   * Callback triggered before parsing.
   */
  onBeforeParse(content: string): string {
    return content;
  }
 
  /**
   * Callback triggered after parsing.
   */
  onAfterParse(content: ParsedNodes): ParsedNodes {
    return content;
  }
}