interweave
Version:
React library to safely render HTML, filter attributes, autowrap text, autolink, and much more.
415 lines (370 loc) • 10.4 kB
JavaScript
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
// Bundled with Packemon: https://packemon.dev
// Platform: browser, Support: stable, Format: esm
import React from 'react';
/* eslint-disable no-bitwise, no-magic-numbers, sort-keys */
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories
const TYPE_FLOW = 1;
const TYPE_SECTION = 1 << 1;
const TYPE_HEADING = 1 << 2;
const TYPE_PHRASING = 1 << 3;
const TYPE_EMBEDDED = 1 << 4;
const TYPE_INTERACTIVE = 1 << 5;
const TYPE_PALPABLE = 1 << 6; // https://developer.mozilla.org/en-US/docs/Web/HTML/Element
const tagConfigs = {
a: {
content: TYPE_FLOW | TYPE_PHRASING,
self: false,
type: TYPE_FLOW | TYPE_PHRASING | TYPE_INTERACTIVE | TYPE_PALPABLE
},
address: {
invalid: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'address', 'article', 'aside', 'section', 'div', 'header', 'footer'],
self: false
},
audio: {
children: ['track', 'source']
},
br: {
type: TYPE_FLOW | TYPE_PHRASING,
void: true
},
body: {
content: TYPE_FLOW | TYPE_SECTION | TYPE_HEADING | TYPE_PHRASING | TYPE_EMBEDDED | TYPE_INTERACTIVE | TYPE_PALPABLE
},
button: {
content: TYPE_PHRASING,
type: TYPE_FLOW | TYPE_PHRASING | TYPE_INTERACTIVE | TYPE_PALPABLE
},
caption: {
content: TYPE_FLOW,
parent: ['table']
},
col: {
parent: ['colgroup'],
void: true
},
colgroup: {
children: ['col'],
parent: ['table']
},
details: {
children: ['summary'],
type: TYPE_FLOW | TYPE_INTERACTIVE | TYPE_PALPABLE
},
dd: {
content: TYPE_FLOW,
parent: ['dl']
},
dl: {
children: ['dt', 'dd'],
type: TYPE_FLOW
},
dt: {
content: TYPE_FLOW,
invalid: ['footer', 'header'],
parent: ['dl']
},
figcaption: {
content: TYPE_FLOW,
parent: ['figure']
},
footer: {
invalid: ['footer', 'header']
},
header: {
invalid: ['footer', 'header']
},
hr: {
type: TYPE_FLOW,
void: true
},
img: {
void: true
},
li: {
content: TYPE_FLOW,
parent: ['ul', 'ol', 'menu']
},
main: {
self: false
},
ol: {
children: ['li'],
type: TYPE_FLOW
},
picture: {
children: ['source', 'img'],
type: TYPE_FLOW | TYPE_PHRASING | TYPE_EMBEDDED
},
rb: {
parent: ['ruby', 'rtc']
},
rp: {
parent: ['ruby', 'rtc']
},
rt: {
content: TYPE_PHRASING,
parent: ['ruby', 'rtc']
},
rtc: {
content: TYPE_PHRASING,
parent: ['ruby']
},
ruby: {
children: ['rb', 'rp', 'rt', 'rtc']
},
source: {
parent: ['audio', 'video', 'picture'],
void: true
},
summary: {
content: TYPE_PHRASING,
parent: ['details']
},
table: {
children: ['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr'],
type: TYPE_FLOW
},
tbody: {
parent: ['table'],
children: ['tr']
},
td: {
content: TYPE_FLOW,
parent: ['tr']
},
tfoot: {
parent: ['table'],
children: ['tr']
},
th: {
content: TYPE_FLOW,
parent: ['tr']
},
thead: {
parent: ['table'],
children: ['tr']
},
tr: {
parent: ['table', 'tbody', 'thead', 'tfoot'],
children: ['th', 'td']
},
track: {
parent: ['audio', 'video'],
void: true
},
ul: {
children: ['li'],
type: TYPE_FLOW
},
video: {
children: ['track', 'source']
},
wbr: {
type: TYPE_FLOW | TYPE_PHRASING,
void: true
}
};
function createConfigBuilder(config) {
return tagName => {
tagConfigs[tagName] = { ...config,
...tagConfigs[tagName]
};
};
}
['address', 'main', 'div', 'figure', 'p', 'pre'].forEach(createConfigBuilder({
content: TYPE_FLOW,
type: TYPE_FLOW | TYPE_PALPABLE
}));
['abbr', 'b', 'bdi', 'bdo', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd', 'mark', 'q', 'ruby', 'samp', 'strong', 'sub', 'sup', 'time', 'u', 'var'].forEach(createConfigBuilder({
content: TYPE_PHRASING,
type: TYPE_FLOW | TYPE_PHRASING | TYPE_PALPABLE
}));
['p', 'pre'].forEach(createConfigBuilder({
content: TYPE_PHRASING,
type: TYPE_FLOW | TYPE_PALPABLE
}));
['s', 'small', 'span', 'del', 'ins'].forEach(createConfigBuilder({
content: TYPE_PHRASING,
type: TYPE_FLOW | TYPE_PHRASING
}));
['article', 'aside', 'footer', 'header', 'nav', 'section', 'blockquote'].forEach(createConfigBuilder({
content: TYPE_FLOW,
type: TYPE_FLOW | TYPE_SECTION | TYPE_PALPABLE
}));
['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(createConfigBuilder({
content: TYPE_PHRASING,
type: TYPE_FLOW | TYPE_HEADING | TYPE_PALPABLE
}));
['audio', 'canvas', 'iframe', 'img', 'video'].forEach(createConfigBuilder({
type: TYPE_FLOW | TYPE_PHRASING | TYPE_EMBEDDED | TYPE_PALPABLE
})); // Disable this map from being modified
const TAGS = Object.freeze(tagConfigs); // Tags that should never be allowed, even if the allow list is disabled
const BANNED_TAG_LIST = ['applet', 'base', 'body', 'command', 'embed', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noscript', 'object', 'script', 'style', 'title'];
const ALLOWED_TAG_LIST = Object.keys(TAGS).filter(tag => tag !== 'canvas' && tag !== 'iframe'); // Filters apply to HTML attributes
const FILTER_ALLOW = 1;
const FILTER_DENY = 2;
const FILTER_CAST_NUMBER = 3;
const FILTER_CAST_BOOL = 4;
const FILTER_NO_CAST = 5; // Attributes not listed here will be denied
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
const ATTRIBUTES = Object.freeze({
alt: FILTER_ALLOW,
cite: FILTER_ALLOW,
class: FILTER_ALLOW,
colspan: FILTER_CAST_NUMBER,
controls: FILTER_CAST_BOOL,
datetime: FILTER_ALLOW,
default: FILTER_CAST_BOOL,
disabled: FILTER_CAST_BOOL,
dir: FILTER_ALLOW,
height: FILTER_ALLOW,
href: FILTER_ALLOW,
id: FILTER_ALLOW,
kind: FILTER_ALLOW,
label: FILTER_ALLOW,
lang: FILTER_ALLOW,
loading: FILTER_ALLOW,
loop: FILTER_CAST_BOOL,
media: FILTER_ALLOW,
muted: FILTER_CAST_BOOL,
poster: FILTER_ALLOW,
rel: FILTER_ALLOW,
role: FILTER_ALLOW,
rowspan: FILTER_CAST_NUMBER,
scope: FILTER_ALLOW,
sizes: FILTER_ALLOW,
span: FILTER_CAST_NUMBER,
start: FILTER_CAST_NUMBER,
style: FILTER_NO_CAST,
src: FILTER_ALLOW,
srclang: FILTER_ALLOW,
srcset: FILTER_ALLOW,
tabindex: FILTER_ALLOW,
target: FILTER_ALLOW,
title: FILTER_ALLOW,
type: FILTER_ALLOW,
width: FILTER_ALLOW
}); // Attributes to camel case for React props
const ATTRIBUTES_TO_PROPS = Object.freeze({
class: 'className',
colspan: 'colSpan',
datetime: 'dateTime',
rowspan: 'rowSpan',
srclang: 'srcLang',
srcset: 'srcSet',
tabindex: 'tabIndex'
});
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function Element({
attributes = {},
className,
children = null,
selfClose = false,
tagName
}) {
const Tag = tagName;
return selfClose ? /*#__PURE__*/React.createElement(Tag, _extends({
className: className
}, attributes)) : /*#__PURE__*/React.createElement(Tag, _extends({
className: className
}, attributes), children);
}
class Filter {
/**
* Filter and clean an HTML attribute value.
*/
attribute(name, value) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return value;
}
/**
* Filter and clean an HTML node.
*/
node(name, node) {
return node;
}
}
/**
* Trigger the actual pattern match and package the matched
* response through a callback.
*/
function match(string, pattern, process, isVoid = false) {
const matches = string.match(pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i'));
if (!matches) {
return null;
}
return {
match: matches[0],
void: isVoid,
...process(matches),
index: matches.index,
length: matches[0].length,
valid: true
};
}
class Matcher {
constructor(name, options, factory) {
_defineProperty(this, "greedy", false);
_defineProperty(this, "options", void 0);
_defineProperty(this, "propName", void 0);
_defineProperty(this, "inverseName", void 0);
_defineProperty(this, "factory", void 0);
if (process.env.NODE_ENV !== "production" && (!name || name.toLowerCase() === 'html')) {
throw new Error(`The matcher name "${name}" is not allowed.`);
} // @ts-expect-error Allow override
this.options = { ...options
};
this.propName = name;
this.inverseName = `no${name.charAt(0).toUpperCase() + name.slice(1)}`;
this.factory = factory !== null && factory !== void 0 ? factory : null;
}
/**
* Attempts to create a React element using a custom user provided factory,
* or the default matcher factory.
*/
createElement(children, props) {
const element = this.factory ? /*#__PURE__*/React.createElement(this.factory, props, children) : this.replaceWith(children, props);
if (process.env.NODE_ENV !== "production" && typeof element !== 'string' && ! /*#__PURE__*/React.isValidElement(element)) {
throw new Error(`Invalid React element created from ${this.constructor.name}.`);
}
return element;
}
/**
* Trigger the actual pattern match and package the matched
* response through a callback.
*/
doMatch(string, pattern, callback, isVoid = false) {
return match(string, pattern, callback, isVoid);
}
/**
* Callback triggered before parsing.
*/
onBeforeParse(content, props) {
return content;
}
/**
* Callback triggered after parsing.
*/
onAfterParse(content, props) {
return content;
}
/**
* Replace the match with a React element based on the matched token and optional props.
*/
}
export { ALLOWED_TAG_LIST as A, BANNED_TAG_LIST as B, Element as E, Filter as F, Matcher as M, TAGS as T, _extends as _, ATTRIBUTES as a, FILTER_DENY as b, ATTRIBUTES_TO_PROPS as c, FILTER_CAST_BOOL as d, FILTER_CAST_NUMBER as e, FILTER_NO_CAST as f, TYPE_FLOW as g, TYPE_SECTION as h, TYPE_HEADING as i, TYPE_PHRASING as j, TYPE_EMBEDDED as k, TYPE_INTERACTIVE as l, TYPE_PALPABLE as m, FILTER_ALLOW as n, match as o };
//# sourceMappingURL=bundle-7aab7250.js.map