react-native-parsed-text
Version:
Parse text and make them into multiple React Native Text elements
137 lines (120 loc) • 4.65 kB
JavaScript
import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import TextExtraction from './lib/TextExtraction';
/**
* This is a list of the known patterns that are provided by this library
* @typedef {('url'|'phone'|'email')} KnownParsePattern
*/
/**
* @type {Object.<string, RegExp>}
* // The keys really should be KnownParsePattern -- but this is unsupported in jsdoc, sadly
*/
export const PATTERNS = {
/**
* Segments/Features:
* - http/https support https?
* - auto-detecting loose domains if preceded by `www.`
* - Localized & Long top-level domains \.(xn--)?[a-z0-9-]{2,20}\b
* - Allowed query parameters & values, it's two blocks of matchers
* ([-a-zA-Z0-9@:%_\+,.~#?&\/=]*[-a-zA-Z0-9@:%_\+~#?&\/=])*
* - First block is [-a-zA-Z0-9@:%_\+\[\],.~#?&\/=]* -- this matches parameter names & values (including commas, dots, opening & closing brackets)
* - The first block must be followed by a closing block [-a-zA-Z0-9@:%_\+\]~#?&\/=] -- this doesn't match commas, dots, and opening brackets
*/
url: /(https?:\/\/|www\.)[-a-zA-Z0-9@:%._\+~#=]{1,256}\.(xn--)?[a-z0-9-]{2,20}\b([-a-zA-Z0-9@:%_\+\[\],.~#?&\/=]*[-a-zA-Z0-9@:%_\+\]~#?&\/=])*/i,
phone: /[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,7}/,
email: /\S+@\S+\.\S+/,
};
/**
* This is for built-in-patterns already supported by this library
* Note: any additional keys/props are permitted, and will be passed along as props to the <Text> component!
* @typedef {Object} DefaultParseShape
* @property {KnownParsePattern} [type] key of the known pattern you'd like to configure
* @property {number} [nonExhaustiveModeMaxMatchCount] Enables "non-exhaustive mode", where you can limit how many matches are found. -- Must be a positive integer or Infinity matches are permitted
* @property {Function} [renderText] arbitrary function to rewrite the matched string into something else
* @property {Function} [onPress]
* @property {Function} [onLongPress]
*/
const defaultParseShape = PropTypes.shape({
...Text.propTypes,
type: PropTypes.oneOf(Object.keys(PATTERNS)).isRequired,
nonExhaustiveMaxMatchCount: PropTypes.number,
});
const customParseShape = PropTypes.shape({
...Text.propTypes,
pattern: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(RegExp)])
.isRequired,
nonExhaustiveMaxMatchCount: PropTypes.number,
});
/**
* The props added by this component
* @typedef {DefaultParseShape|import('./lib/TextExtraction').CustomParseShape} ParsedTextAddedProps
* @property {ParseShape[]} parse
* @property {import('react-native').TextProps} childrenProps -- the props set on each child Text component
*/
/** @typedef {ParsedTextAddedProps & import('react-native').TextProps} ParsedTextProps */
/** @type {import('react').ComponentClass<ParsedTextProps>} */
class ParsedText extends React.Component {
static displayName = 'ParsedText';
static propTypes = {
...Text.propTypes,
parse: PropTypes.arrayOf(
PropTypes.oneOfType([defaultParseShape, customParseShape]),
),
childrenProps: PropTypes.shape(Text.propTypes),
};
static defaultProps = {
parse: null,
childrenProps: {},
};
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps);
}
/** @returns {import('./lib/TextExtraction').CustomParseShape[]} */
getPatterns() {
return this.props.parse.map((option) => {
const { type, ...patternOption } = option;
if (type) {
if (!PATTERNS[type]) {
throw new Error(`${option.type} is not a supported type`);
}
patternOption.pattern = PATTERNS[type];
}
return patternOption;
});
}
getParsedText() {
if (!this.props.parse) {
return this.props.children;
}
if (typeof this.props.children !== 'string') {
return this.props.children;
}
const textExtraction = new TextExtraction(
this.props.children,
this.getPatterns(),
);
return textExtraction.parse().map((props, index) => {
const { style: parentStyle } = this.props;
const { style, ...remainder } = props;
return (
<Text
key={`parsedText-${index}`}
style={[parentStyle, style]}
{...this.props.childrenProps}
{...remainder}
/>
);
});
}
render() {
// Discard custom props before passing remainder to Text
const { parse, childrenProps, ...remainder } = { ...this.props };
return (
<Text ref={(ref) => (this._root = ref)} {...remainder}>
{this.getParsedText()}
</Text>
);
}
}
export default ParsedText;