From 76a35ab27c9295b408fc0d7544207e4a5ca1c821 Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Fri, 11 Mar 2022 16:52:37 -0800
Subject: [PATCH 01/18] Add new APIs.
---
packages/core/src/createMatcher.ts | 99 ++++++++++++++++++++++++++
packages/core/src/createTransformer.ts | 49 +++++++++++++
packages/core/src/types.ts | 37 ++++++++++
3 files changed, 185 insertions(+)
create mode 100644 packages/core/src/createMatcher.ts
create mode 100644 packages/core/src/createTransformer.ts
diff --git a/packages/core/src/createMatcher.ts b/packages/core/src/createMatcher.ts
new file mode 100644
index 00000000..dc265df3
--- /dev/null
+++ b/packages/core/src/createMatcher.ts
@@ -0,0 +1,99 @@
+import { CommonInternals, OnAfterParse, OnBeforeParse, TagName } from './types';
+
+export type OnMatch = (
+ result: MatchResult,
+ props: Props,
+ options: Partial,
+) => Match | null;
+
+export interface MatchResult {
+ index: number;
+ length: number;
+ match: string;
+ matches: string[];
+ valid: boolean;
+ value: string;
+ void: boolean;
+}
+
+export type MatchHandler = (
+ value: string,
+ props: Props,
+) => (MatchResult & { params: Match }) | null;
+
+export interface MatcherOptions {
+ greedy?: boolean;
+ tagName: TagName;
+ void?: boolean;
+ options?: Options;
+ onAfterParse?: OnAfterParse;
+ onBeforeParse?: OnBeforeParse;
+ onMatch: OnMatch;
+}
+
+export type MatcherFactory = (
+ match: Match,
+ props: Props,
+ content: Node,
+) => React.ReactElement;
+
+export interface Matcher extends CommonInternals {
+ extend: (
+ factory?: MatcherFactory | null,
+ options?: Partial>,
+ ) => Matcher;
+ factory: MatcherFactory;
+ greedy: boolean;
+ match: MatchHandler;
+ tagName: TagName;
+}
+
+export function createMatcher(
+ pattern: RegExp | string,
+ factory: MatcherFactory,
+ options: MatcherOptions,
+): Matcher {
+ return {
+ extend(customFactory, customOptions) {
+ return createMatcher(pattern, customFactory ?? factory, {
+ ...options,
+ ...customOptions,
+ });
+ },
+ factory,
+ greedy: options.greedy ?? false,
+ match(value, props) {
+ const matches = value.match(pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i'));
+
+ if (!matches) {
+ return null;
+ }
+
+ const result: MatchResult = {
+ index: matches.index!,
+ length: matches[0].length,
+ match: matches[0],
+ matches,
+ valid: true,
+ value,
+ void: options.void ?? false,
+ };
+
+ const params = options.onMatch(result, props, options.options ?? {});
+
+ // Allow callback to intercept the result
+ if (params === null) {
+ return null;
+ }
+
+ return {
+ params,
+ ...result,
+ };
+ },
+ onAfterParse: options.onAfterParse,
+ onBeforeParse: options.onBeforeParse,
+ options: options.options ?? {},
+ tagName: options.tagName,
+ };
+}
diff --git a/packages/core/src/createTransformer.ts b/packages/core/src/createTransformer.ts
new file mode 100644
index 00000000..a693ad5f
--- /dev/null
+++ b/packages/core/src/createTransformer.ts
@@ -0,0 +1,49 @@
+import { CommonInternals, OnAfterParse, OnBeforeParse, TagName, WildTagName } from './types';
+
+export type InferElement = K extends '*'
+ ? HTMLElement
+ : K extends keyof HTMLElementTagNameMap
+ ? HTMLElementTagNameMap[K]
+ : HTMLElement;
+
+export type TransformerFactory = (
+ element: Element,
+ props: Props,
+ content: Node,
+) => Element | React.ReactElement | null | undefined | void;
+
+export interface TransformerOptions {
+ tagName?: TagName;
+ onAfterParse?: OnAfterParse;
+ onBeforeParse?: OnBeforeParse;
+ options?: Options;
+}
+
+export interface Transformer extends CommonInternals {
+ extend: (
+ factory?: TransformerFactory | null,
+ options?: Partial>,
+ ) => Transformer;
+ factory: TransformerFactory;
+ tagName: WildTagName;
+}
+
+export function createTransformer(
+ tagName: K,
+ factory: TransformerFactory, Props>,
+ options: TransformerOptions = {},
+): Transformer, Props, Options> {
+ return {
+ extend(customFactory, customOptions) {
+ return createTransformer(tagName, customFactory ?? factory, {
+ ...options,
+ ...customOptions,
+ });
+ },
+ factory,
+ onAfterParse: options.onAfterParse,
+ onBeforeParse: options.onBeforeParse,
+ options: options.options ?? {},
+ tagName: options.tagName ?? tagName,
+ };
+}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 8e66f23e..ef0ca3bd 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -7,6 +7,43 @@ declare global {
var INTERWEAVE_SSR_POLYFILL: (() => Document | undefined) | undefined;
}
+export interface CommonInternals {
+ onAfterParse?: OnAfterParse;
+ onBeforeParse?: OnBeforeParse;
+ options: Partial;
+}
+
+// ELEMENTS
+
+export type TagName = keyof React.ReactHTML | 'rb' | 'rtc';
+
+export type WildTagName = TagName | '*';
+
+export interface TagConfig {
+ // Only children
+ children: TagName[];
+ // Children content type
+ content: number;
+ // Invalid children
+ invalid: TagName[];
+ // Only parent
+ parent: TagName[];
+ // Can render self as a child
+ self: boolean;
+ // HTML tag name
+ tagName: TagName;
+ // Self content type
+ type: number;
+ // Self-closing tag
+ void: boolean;
+}
+
+// CALLBACKS
+
+export type OnAfterParse = (content: Node, props: Props) => Node;
+
+export type OnBeforeParse = (content: string, props: Props) => string;
+
export type Node = React.ReactElement | string | null;
export type ChildrenNode = Node[] | string;
From 053335c191e64f48ae1089c3c333e413f7c13ca5 Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Fri, 11 Mar 2022 17:00:05 -0800
Subject: [PATCH 02/18] Remove old code.
---
packages/core/src/Filter.ts | 21 --------
packages/core/src/Matcher.ts | 88 -------------------------------
packages/core/src/Parser.ts | 25 ++++-----
packages/core/src/StyleFilter.ts | 23 --------
packages/core/src/constants.ts | 8 +--
packages/core/src/index.ts | 7 +--
packages/core/src/match.ts | 27 ----------
packages/core/src/test.tsx | 4 +-
packages/core/src/transformers.ts | 14 +++++
packages/core/src/types.ts | 25 ++-------
10 files changed, 38 insertions(+), 204 deletions(-)
delete mode 100644 packages/core/src/Filter.ts
delete mode 100644 packages/core/src/Matcher.ts
delete mode 100644 packages/core/src/StyleFilter.ts
delete mode 100644 packages/core/src/match.ts
create mode 100644 packages/core/src/transformers.ts
diff --git a/packages/core/src/Filter.ts b/packages/core/src/Filter.ts
deleted file mode 100644
index c5cc6e5a..00000000
--- a/packages/core/src/Filter.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ElementAttributes, FilterInterface } from './types';
-
-export class Filter implements FilterInterface {
- /**
- * Filter and clean an HTML attribute value.
- */
- attribute(
- name: K,
- value: ElementAttributes[K],
- ): ElementAttributes[K] | null | undefined {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return value;
- }
-
- /**
- * Filter and clean an HTML node.
- */
- node(name: string, node: HTMLElement): HTMLElement | null {
- return node;
- }
-}
diff --git a/packages/core/src/Matcher.ts b/packages/core/src/Matcher.ts
deleted file mode 100644
index 51fd6451..00000000
--- a/packages/core/src/Matcher.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from 'react';
-import { match } from './match';
-import { ChildrenNode, MatchCallback, MatcherInterface, MatchResponse, Node } from './types';
-
-export abstract class Matcher
- implements MatcherInterface {
- greedy: boolean = false;
-
- options: Options;
-
- propName: string;
-
- inverseName: string;
-
- factory: React.ComponentType | null;
-
- constructor(name: string, options?: Options, factory?: React.ComponentType | null) {
- if (__DEV__ && (!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;
- }
-
- /**
- * Attempts to create a React element using a custom user provided factory,
- * or the default matcher factory.
- */
- createElement(children: ChildrenNode, props: Props): Node {
- const element = this.factory
- ? React.createElement(this.factory, props, children)
- : this.replaceWith(children, props);
-
- if (__DEV__ && typeof element !== 'string' && !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: string,
- pattern: RegExp | string,
- callback: MatchCallback,
- isVoid: boolean = false,
- ): MatchResponse | null {
- return match(string, pattern, callback, isVoid);
- }
-
- /**
- * Callback triggered before parsing.
- */
- onBeforeParse(content: string, props: Props): string {
- return content;
- }
-
- /**
- * Callback triggered after parsing.
- */
- onAfterParse(content: Node[], props: Props): Node[] {
- return content;
- }
-
- /**
- * Replace the match with a React element based on the matched token and optional props.
- */
- abstract replaceWith(children: ChildrenNode, props: Props): Node;
-
- /**
- * Defines the HTML tag name that the resulting React element will be.
- */
- abstract asTag(): string;
-
- /**
- * Attempt to match against the defined string. Return `null` if no match found,
- * else return the `match` and any optional props to pass along.
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents
- abstract match(string: string): MatchResponse | null;
-}
diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts
index 4af75ea2..6504541d 100644
--- a/packages/core/src/Parser.ts
+++ b/packages/core/src/Parser.ts
@@ -25,8 +25,9 @@ import {
MatcherElementsMap,
MatcherInterface,
Node,
- NodeConfig,
ParserProps,
+ TagConfig,
+ TagName,
} from './types';
const ELEMENT_NODE = 1;
@@ -118,7 +119,7 @@ export class Parser {
* If a match is found, create a React element, and build a new array.
* This array allows React to interpolate and render accordingly.
*/
- applyMatchers(string: string, parentConfig: NodeConfig): ChildrenNode {
+ applyMatchers(string: string, parentConfig: TagConfig): ChildrenNode {
const elements: MatcherElementsMap = {};
const { props } = this;
let matchedString = string;
@@ -200,7 +201,7 @@ export class Parser {
/**
* Determine whether the child can be rendered within the parent.
*/
- canRenderChild(parentConfig: NodeConfig, childConfig: NodeConfig): boolean {
+ canRenderChild(parentConfig: TagConfig, childConfig: TagConfig): boolean {
if (!parentConfig.tagName || !childConfig.tagName) {
return false;
}
@@ -371,14 +372,15 @@ export class Parser {
/**
* Return configuration for a specific tag.
*/
- getTagConfig(tagName: string): NodeConfig {
- const common = {
+ getTagConfig(baseTagName: string): TagConfig {
+ const tagName = baseTagName as TagName;
+ const common: TagConfig = {
children: [],
content: 0,
invalid: [],
parent: [],
self: true,
- tagName: '',
+ tagName: 'div',
type: 0,
void: false,
};
@@ -454,14 +456,9 @@ export class Parser {
* Loop over the nodes children and generate a
* list of text nodes and React elements.
*/
- parseNode(parentNode: HTMLElement, parentConfig: NodeConfig): Node[] {
- const {
- noHtml,
- noHtmlExceptMatchers,
- allowElements,
- transform,
- transformOnlyAllowList,
- } = this.props;
+ parseNode(parentNode: HTMLElement, parentConfig: TagConfig): Node[] {
+ const { noHtml, noHtmlExceptMatchers, allowElements, transform, transformOnlyAllowList } =
+ this.props;
let content: Node[] = [];
let mergedText = '';
diff --git a/packages/core/src/StyleFilter.ts b/packages/core/src/StyleFilter.ts
deleted file mode 100644
index ea0c5b39..00000000
--- a/packages/core/src/StyleFilter.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Filter } from './Filter';
-import { ElementAttributes } from './types';
-
-const INVALID_STYLES = /(url|image|image-set)\(/i;
-
-export class StyleFilter extends Filter {
- override attribute(
- name: K,
- value: ElementAttributes[K],
- ): ElementAttributes[K] {
- if (name === 'style') {
- Object.keys(value).forEach((key) => {
- if (String(value[key]).match(INVALID_STYLES)) {
- // eslint-disable-next-line no-param-reassign
- delete value[key];
- }
- });
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return value;
- }
-}
diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts
index 8634be0e..abf50c7f 100644
--- a/packages/core/src/constants.ts
+++ b/packages/core/src/constants.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-bitwise, no-magic-numbers, sort-keys */
-import { ConfigMap, FilterMap, NodeConfig } from './types';
+import { FilterMap, TagConfig, TagConfigMap } from './types';
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories
export const TYPE_FLOW = 1;
@@ -12,7 +12,7 @@ export const TYPE_INTERACTIVE = 1 << 5;
export const TYPE_PALPABLE = 1 << 6;
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element
-const tagConfigs: Record> = {
+const tagConfigs: TagConfigMap = {
a: {
content: TYPE_FLOW | TYPE_PHRASING,
self: false,
@@ -188,7 +188,7 @@ const tagConfigs: Record> = {
},
};
-function createConfigBuilder(config: Partial): (tagName: string) => void {
+function createConfigBuilder(config: Partial): (tagName: string) => void {
return (tagName: string) => {
tagConfigs[tagName] = {
...config,
@@ -268,7 +268,7 @@ function createConfigBuilder(config: Partial): (tagName: string) =>
);
// Disable this map from being modified
-export const TAGS: ConfigMap = Object.freeze(tagConfigs);
+export const TAGS: TagConfigMap = Object.freeze(tagConfigs);
// Tags that should never be allowed, even if the allow list is disabled
export const BANNED_TAG_LIST = [
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index bf523d32..dd798979 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -5,10 +5,11 @@
export * from './constants';
export * from './Element';
-export * from './Filter';
export * from './Interweave';
export * from './Markup';
-export * from './match';
-export * from './Matcher';
export * from './Parser';
export * from './types';
+
+// NEW
+export * from './createMatcher';
+export * from './createTransformer';
diff --git a/packages/core/src/match.ts b/packages/core/src/match.ts
deleted file mode 100644
index b87cfcf3..00000000
--- a/packages/core/src/match.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { MatchCallback, MatchResponse } from './types';
-
-/**
- * Trigger the actual pattern match and package the matched
- * response through a callback.
- */
-export function match(
- string: string,
- pattern: RegExp | string,
- process: MatchCallback,
- isVoid: boolean = false,
-): MatchResponse | null {
- 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,
- };
-}
diff --git a/packages/core/src/test.tsx b/packages/core/src/test.tsx
index 61f82e84..1a3deed2 100644
--- a/packages/core/src/test.tsx
+++ b/packages/core/src/test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { ChildrenNode, Element, Filter, Matcher, MatchResponse, Node, NodeConfig, TAGS } from '.';
+import { ChildrenNode, Element, Filter, Matcher, MatchResponse, Node, TagConfig, TAGS } from '.';
export const TOKEN_LOCATIONS = [
'no tokens',
@@ -84,7 +84,7 @@ export const MOCK_INVALID_MARKUP = `
More text with outdated stuff .
`;
-export const parentConfig: NodeConfig = {
+export const parentConfig: TagConfig = {
children: [],
content: 0,
invalid: [],
diff --git a/packages/core/src/transformers.ts b/packages/core/src/transformers.ts
new file mode 100644
index 00000000..60ebb9fc
--- /dev/null
+++ b/packages/core/src/transformers.ts
@@ -0,0 +1,14 @@
+import { createTransformer } from './createTransformer';
+
+const INVALID_STYLES = /(url|image|image-set)\(/i;
+
+export const styleTransformer = createTransformer('*', (element) => {
+ Object.keys(element.style).forEach((k) => {
+ const key = k as keyof typeof element.style;
+
+ if (String(element.style[key]).match(INVALID_STYLES)) {
+ // eslint-disable-next-line no-param-reassign
+ delete element.style[key];
+ }
+ });
+});
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index ef0ca3bd..d95d7683 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -38,6 +38,8 @@ export interface TagConfig {
void: boolean;
}
+export type TagConfigMap = Record>;
+
// CALLBACKS
export type OnAfterParse = (content: Node, props: Props) => Node;
@@ -48,27 +50,6 @@ export type Node = React.ReactElement | string | null;
export type ChildrenNode = Node[] | string;
-export interface NodeConfig {
- // Only children
- children: string[];
- // Children content type
- content: number;
- // Invalid children
- invalid: string[];
- // Only parent
- parent: string[];
- // Can render self as a child
- self: boolean;
- // HTML tag name
- tagName: string;
- // Self content type
- type: number;
- // Self-closing tag
- void: boolean;
-}
-
-export type ConfigMap = Record>;
-
export type AttributeValue = boolean | number | object | string;
export type Attributes = Record;
@@ -80,7 +61,7 @@ export type BeforeParseCallback = (content: string, props: T) => string;
export type TransformCallback = (
node: HTMLElement,
children: Node[],
- config: NodeConfig,
+ config: TagConfig,
) => React.ReactNode;
// MATCHERS
From 8312687c4c9b0e545d7165d19bc2ef56b708c4fd Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Fri, 11 Mar 2022 17:24:30 -0800
Subject: [PATCH 03/18] Convert parser.
---
packages/core/src/Parser.ts | 208 ++++++++++++-------------
packages/core/src/createMatcher.ts | 2 +-
packages/core/src/createTransformer.ts | 2 +-
packages/core/src/types.ts | 36 +----
4 files changed, 107 insertions(+), 141 deletions(-)
diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts
index 6504541d..73c190ba 100644
--- a/packages/core/src/Parser.ts
+++ b/packages/core/src/Parser.ts
@@ -1,4 +1,4 @@
-/* eslint-disable no-bitwise, no-cond-assign, complexity, @typescript-eslint/no-unsafe-return */
+/* eslint-disable no-bitwise, no-cond-assign, complexity */
import React from 'react';
import escapeHtml from 'escape-html';
@@ -13,23 +13,33 @@ import {
FILTER_NO_CAST,
TAGS,
} from './constants';
+import { Matcher } from './createMatcher';
+import { Transformer } from './createTransformer';
import { Element } from './Element';
-import { StyleFilter } from './StyleFilter';
+import { styleTransformer } from './transformers';
import {
Attributes,
AttributeValue,
ChildrenNode,
- ElementAttributes,
ElementProps,
- FilterInterface,
- MatcherElementsMap,
- MatcherInterface,
Node,
ParserProps,
TagConfig,
TagName,
} from './types';
+type TransformerInterface = Transformer;
+
+type MatcherInterface = Matcher;
+
+type MatchedElements = Record<
+ string,
+ {
+ element: React.ReactElement;
+ key: number;
+ }
+>;
+
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const INVALID_ROOTS = /^<(!doctype|(html|head|body)(\s|>))/i;
@@ -46,29 +56,29 @@ function createDocument() {
}
export class Parser {
- allowed: Set;
+ allowed: Set;
- banned: Set;
+ banned: Set;
- blocked: Set;
+ blocked: Set;
container?: HTMLElement;
- content: Node[] = [];
+ content: Node = '';
+
+ keyIndex: number = -1;
props: ParserProps;
matchers: MatcherInterface[];
- filters: FilterInterface[];
-
- keyIndex: number;
+ transformers: TransformerInterface[];
constructor(
markup: string,
- props: ParserProps = {},
+ props: ParserProps,
matchers: MatcherInterface[] = [],
- filters: FilterInterface[] = [],
+ transformers: TransformerInterface[] = [],
) {
if (__DEV__ && markup && typeof markup !== 'string') {
throw new TypeError('Interweave parser requires a valid string.');
@@ -76,42 +86,12 @@ export class Parser {
this.props = props;
this.matchers = matchers;
- this.filters = [...filters, new StyleFilter()];
+ this.transformers = [...transformers, styleTransformer];
this.keyIndex = -1;
this.container = this.createContainer(markup || '');
- this.allowed = new Set(props.allowList ?? ALLOWED_TAG_LIST);
- this.banned = new Set(BANNED_TAG_LIST);
- this.blocked = new Set(props.blockList);
- }
-
- /**
- * Loop through and apply all registered attribute filters.
- */
- applyAttributeFilters(
- name: K,
- value: ElementAttributes[K],
- ): ElementAttributes[K] {
- return this.filters.reduce(
- (nextValue, filter) =>
- nextValue !== null && typeof filter.attribute === 'function'
- ? filter.attribute(name, nextValue)
- : nextValue,
- value,
- );
- }
-
- /**
- * Loop through and apply all registered node filters.
- */
- applyNodeFilters(name: string, node: HTMLElement | null): HTMLElement | null {
- // Allow null to be returned
- return this.filters.reduce(
- (nextNode, filter) =>
- nextNode !== null && typeof filter.node === 'function'
- ? filter.node(name, nextNode)
- : nextNode,
- node,
- );
+ this.allowed = new Set(props.allow ?? (ALLOWED_TAG_LIST as TagName[]));
+ this.banned = new Set(BANNED_TAG_LIST as TagName[]);
+ this.blocked = new Set(props.block);
}
/**
@@ -120,18 +100,17 @@ export class Parser {
* This array allows React to interpolate and render accordingly.
*/
applyMatchers(string: string, parentConfig: TagConfig): ChildrenNode {
- const elements: MatcherElementsMap = {};
- const { props } = this;
+ const elements: MatchedElements = {};
let matchedString = string;
let elementIndex = 0;
let parts = null;
this.matchers.forEach((matcher) => {
- const tagName = matcher.asTag().toLowerCase();
+ const { tagName } = matcher;
const config = this.getTagConfig(tagName);
// Skip matchers that have been disabled from props or are not supported
- if ((props as Record)[matcher.inverseName] || !this.isTagAllowed(tagName)) {
+ if (!this.isTagAllowed(tagName)) {
return;
}
@@ -143,9 +122,9 @@ export class Parser {
// Continuously trigger the matcher until no matches are found
let tokenizedString = '';
- while (matchedString && (parts = matcher.match(matchedString))) {
- const { index, length, match, valid, void: isVoid, ...partProps } = parts;
- const tokenName = matcher.propName + String(elementIndex);
+ while (matchedString && (parts = matcher.match(matchedString, this.props))) {
+ const { index, length, match, valid, void: isVoid, params } = parts;
+ const tokenName = tagName + String(elementIndex);
// Piece together a new string with interpolated tokens
if (index > 0) {
@@ -161,13 +140,8 @@ export class Parser {
elementIndex += 1;
elements[tokenName] = {
- children: match,
- matcher,
- props: {
- ...props,
- ...partProps,
- key: this.keyIndex,
- },
+ element: matcher.factory(params, this.props, match),
+ key: this.keyIndex,
};
} else {
tokenizedString += match;
@@ -198,6 +172,30 @@ export class Parser {
return this.replaceTokens(matchedString, elements);
}
+ /**
+ * Loop through and apply transformers that match the specific tag name
+ */
+ applyTransformers(
+ tagName: TagName,
+ node: HTMLElement,
+ children: unknown[],
+ ): HTMLElement | React.ReactElement | null | undefined {
+ const transformers = this.transformers.filter(
+ (transformer) => transformer.tagName === tagName || transformer.tagName === '*',
+ );
+
+ for (const transformer of transformers) {
+ const result = transformer.factory(node, this.props, children);
+
+ // If something was returned, the node has been replaced so we cant continue
+ if (result !== undefined) {
+ return result;
+ }
+ }
+
+ return undefined;
+ }
+
/**
* Determine whether the child can be rendered within the parent.
*/
@@ -337,10 +335,7 @@ export class Parser {
newValue = String(newValue);
}
- attributes[ATTRIBUTES_TO_PROPS[newName] || newName] = this.applyAttributeFilters(
- newName as keyof ElementAttributes,
- newValue,
- ) as AttributeValue;
+ attributes[ATTRIBUTES_TO_PROPS[newName] || newName] = newValue;
count += 1;
});
@@ -372,15 +367,14 @@ export class Parser {
/**
* Return configuration for a specific tag.
*/
- getTagConfig(baseTagName: string): TagConfig {
- const tagName = baseTagName as TagName;
+ getTagConfig(tagName: TagName): TagConfig {
const common: TagConfig = {
children: [],
content: 0,
invalid: [],
parent: [],
self: true,
- tagName: 'div',
+ tagName,
type: 0,
void: false,
};
@@ -430,7 +424,7 @@ export class Parser {
/**
* Verify that an HTML tag is allowed to render.
*/
- isTagAllowed(tagName: string): boolean {
+ isTagAllowed(tagName: TagName): boolean {
if (this.banned.has(tagName) || this.blocked.has(tagName)) {
return false;
}
@@ -444,12 +438,15 @@ export class Parser {
* while looping over all child nodes and generating an
* array to interpolate into JSX.
*/
- parse(): Node[] {
+ parse(): React.ReactNode {
if (!this.container) {
- return [];
+ return null;
}
- return this.parseNode(this.container, this.getTagConfig(this.container.nodeName.toLowerCase()));
+ return this.parseNode(
+ this.container,
+ this.getTagConfig(this.container.nodeName.toLowerCase() as TagName),
+ );
}
/**
@@ -457,8 +454,7 @@ export class Parser {
* list of text nodes and React elements.
*/
parseNode(parentNode: HTMLElement, parentConfig: TagConfig): Node[] {
- const { noHtml, noHtmlExceptMatchers, allowElements, transform, transformOnlyAllowList } =
- this.props;
+ const { noHtml, noHtmlExceptMatchers, allowElements } = this.props;
let content: Node[] = [];
let mergedText = '';
@@ -466,8 +462,8 @@ export class Parser {
[...parentNode.childNodes].forEach((node: ChildNode) => {
// Create React elements from HTML elements
if (node.nodeType === ELEMENT_NODE) {
- const tagName = node.nodeName.toLowerCase();
- const config = this.getTagConfig(tagName);
+ let tagName = node.nodeName.toLowerCase() as TagName;
+ let config = this.getTagConfig(tagName);
// Persist any previous text
if (mergedText) {
@@ -475,36 +471,35 @@ export class Parser {
mergedText = '';
}
- // Apply node filters first
- const nextNode = this.applyNodeFilters(tagName, node as HTMLElement);
+ // Increase key before transforming
+ this.keyIndex += 1;
- if (!nextNode) {
- return;
- }
+ // Must occur after key is set
+ const key = this.keyIndex;
+ const children = this.parseNode(node as HTMLElement, config);
- // Apply transformation second
- let children;
+ // Apply transformations to element
+ let nextNode = this.applyTransformers(tagName, node as HTMLElement, children);
- if (transform && !(transformOnlyAllowList && !this.isTagAllowed(tagName))) {
- this.keyIndex += 1;
- const key = this.keyIndex;
-
- // Must occur after key is set
- children = this.parseNode(nextNode, config);
+ // Remove the node entirely
+ if (nextNode === null) {
+ return;
+ }
- const transformed = transform(nextNode, children, config);
+ // Use the node as-is
+ if (nextNode === undefined) {
+ nextNode = node as HTMLElement;
- if (transformed === null) {
- return;
- }
- if (typeof transformed !== 'undefined') {
- content.push(React.cloneElement(transformed as React.ReactElement, { key }));
+ // React element, so apply the key and continue
+ } else if (React.isValidElement(nextNode)) {
+ content.push(React.cloneElement(nextNode, { key }));
- return;
- }
+ return;
- // Reset as we're not using the transformation
- this.keyIndex = key - 1;
+ // HTML element, so update tag and config
+ } else if (nextNode instanceof HTMLElement) {
+ tagName = nextNode.tagName.toLowerCase() as TagName;
+ config = this.getTagConfig(tagName);
}
// Never allow these tags (except via a transformer)
@@ -582,7 +577,7 @@ export class Parser {
* Deconstruct the string into an array, by replacing custom tokens with React elements,
* so that React can render it correctly.
*/
- replaceTokens(tokenizedString: string, elements: MatcherElementsMap): ChildrenNode {
+ replaceTokens(tokenizedString: string, elements: MatchedElements): ChildrenNode {
if (!tokenizedString.includes('{{{')) {
return tokenizedString;
}
@@ -609,14 +604,14 @@ export class Parser {
text = text.slice(startIndex);
}
- const { children, matcher, props: elementProps } = elements[tokenName];
+ const { element, key } = elements[tokenName];
let endIndex: number;
// Use tag as-is if void
if (isVoid) {
endIndex = match.length;
- nodes.push(matcher.createElement(children, elementProps));
+ nodes.push(React.cloneElement(element, { key }));
// Find the closing tag if not void
} else {
@@ -629,9 +624,10 @@ export class Parser {
endIndex = close.index! + close[0].length;
nodes.push(
- matcher.createElement(
+ React.cloneElement(
+ element,
+ { key },
this.replaceTokens(text.slice(match.length, close.index), elements),
- elementProps,
),
);
}
diff --git a/packages/core/src/createMatcher.ts b/packages/core/src/createMatcher.ts
index dc265df3..58a0b7b7 100644
--- a/packages/core/src/createMatcher.ts
+++ b/packages/core/src/createMatcher.ts
@@ -1,4 +1,4 @@
-import { CommonInternals, OnAfterParse, OnBeforeParse, TagName } from './types';
+import { CommonInternals, Node, OnAfterParse, OnBeforeParse, TagName } from './types';
export type OnMatch = (
result: MatchResult,
diff --git a/packages/core/src/createTransformer.ts b/packages/core/src/createTransformer.ts
index a693ad5f..bc1252d3 100644
--- a/packages/core/src/createTransformer.ts
+++ b/packages/core/src/createTransformer.ts
@@ -1,4 +1,4 @@
-import { CommonInternals, OnAfterParse, OnBeforeParse, TagName, WildTagName } from './types';
+import { CommonInternals, Node, OnAfterParse, OnBeforeParse, TagName, WildTagName } from './types';
export type InferElement = K extends '*'
? HTMLElement
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index d95d7683..cad09d76 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -46,7 +46,7 @@ export type OnAfterParse = (content: Node, props: Props) => Node;
export type OnBeforeParse = (content: string, props: Props) => string;
-export type Node = React.ReactElement | string | null;
+export type Node = NonNullable;
export type ChildrenNode = Node[] | string;
@@ -76,51 +76,21 @@ export type MatchResponse = T & {
void?: boolean;
};
-export interface MatcherInterface {
- greedy?: boolean;
- inverseName: string;
- propName: string;
- asTag: () => string;
- createElement: (children: ChildrenNode, props: T) => Node;
- match: (value: string) => MatchResponse> | null;
- onBeforeParse?: (content: string, props: T) => string;
- onAfterParse?: (content: Node[], props: T) => Node[];
-}
-
// FILTERS
export type ElementAttributes = React.AllHTMLAttributes;
-export interface FilterInterface {
- attribute?: (
- name: K,
- value: ElementAttributes[K],
- ) => ElementAttributes[K] | null | undefined;
- node?: (name: string, node: HTMLElement) => HTMLElement | null;
-}
-
-export type FilterMap = Record;
-
// PARSER
-export type MatcherElementsMap = Record<
- string,
- {
- children: string;
- matcher: MatcherInterface<{}>;
- props: object;
- }
->;
-
export interface ParserProps {
/** Disable filtering and allow all non-banned HTML attributes. */
allowAttributes?: boolean;
/** Disable filtering and allow all non-banned/blocked HTML elements to be rendered. */
allowElements?: boolean;
/** List of HTML tag names to allow and render. Defaults to the `ALLOWED_TAG_LIST` constant. */
- allowList?: string[];
+ allow?: TagName[];
/** List of HTML tag names to disallow and not render. Overrides allow list. */
- blockList?: string[];
+ block?: TagName[];
/** Disable the conversion of new lines to ` ` elements. */
disableLineBreaks?: boolean;
/** The container element to parse content in. Applies browser semantic rules and overrides `tagName`. */
From 098c6423fe4c0d6a22d2ebf1c5b3f6fe4773c73c Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Fri, 11 Mar 2022 17:59:56 -0800
Subject: [PATCH 04/18] Update markup.
---
packages/core/src/Markup.tsx | 51 ++++++--------------------
packages/core/src/types.ts | 70 ++++++++----------------------------
2 files changed, 24 insertions(+), 97 deletions(-)
diff --git a/packages/core/src/Markup.tsx b/packages/core/src/Markup.tsx
index 4b647034..aef3cda2 100644
--- a/packages/core/src/Markup.tsx
+++ b/packages/core/src/Markup.tsx
@@ -1,47 +1,16 @@
-/* eslint-disable react/jsx-fragments */
-
-import React from 'react';
-import { Element } from './Element';
+import React, { useMemo } from 'react';
import { Parser } from './Parser';
import { MarkupProps } from './types';
export function Markup(props: MarkupProps) {
- const {
- attributes,
- className,
- containerTagName,
- content,
- emptyContent,
- parsedContent,
- tagName,
- noWrap: baseNoWrap,
- } = props;
- const tag = containerTagName ?? tagName ?? 'span';
- const noWrap = tag === 'fragment' ? true : baseNoWrap;
- let mainContent;
-
- if (parsedContent) {
- mainContent = parsedContent;
- } else {
- const markup = new Parser(content ?? '', props).parse();
-
- if (markup.length > 0) {
- mainContent = markup;
- }
- }
-
- if (!mainContent) {
- mainContent = emptyContent;
- }
-
- if (noWrap) {
- // eslint-disable-next-line react/jsx-no-useless-fragment
- return {mainContent} ;
- }
-
- return (
-
- {mainContent}
-
+ const { content, emptyContent, parsedContent } = props;
+ const mainContent = useMemo(
+ () => parsedContent ?? new Parser(content ?? '', props).parse(),
+ // Do not include `peops` as we only want to re-render on content changes
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [parsedContent, content],
);
+
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>{mainContent ?? emptyContent}>;
}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index cad09d76..08887c50 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
+import type { Matcher } from './createMatcher';
+import type { Transformer } from './createTransformer';
declare global {
// eslint-disable-next-line no-var, vars-on-top
@@ -54,38 +56,12 @@ export type AttributeValue = boolean | number | object | string;
export type Attributes = Record;
-export type AfterParseCallback = (content: Node[], props: T) => Node[];
-
-export type BeforeParseCallback = (content: string, props: T) => string;
-
-export type TransformCallback = (
- node: HTMLElement,
- children: Node[],
- config: TagConfig,
-) => React.ReactNode;
-
-// MATCHERS
-
-export type MatchCallback = (matches: string[]) => T;
-
-export type MatchResponse = T & {
- index: number;
- length: number;
- match: string;
- valid: boolean;
- void?: boolean;
-};
-
-// FILTERS
-
-export type ElementAttributes = React.AllHTMLAttributes;
-
// PARSER
export interface ParserProps {
- /** Disable filtering and allow all non-banned HTML attributes. */
+ /** Allow all non-banned HTML attributes. */
allowAttributes?: boolean;
- /** Disable filtering and allow all non-banned/blocked HTML elements to be rendered. */
+ /** Allow all non-banned and non-blocked HTML elements to be rendered. */
allowElements?: boolean;
/** List of HTML tag names to allow and render. Defaults to the `ALLOWED_TAG_LIST` constant. */
allow?: TagName[];
@@ -93,54 +69,36 @@ export interface ParserProps {
block?: TagName[];
/** Disable the conversion of new lines to ` ` elements. */
disableLineBreaks?: boolean;
- /** The container element to parse content in. Applies browser semantic rules and overrides `tagName`. */
- containerTagName?: string;
/** Escape all HTML before parsing. */
escapeHtml?: boolean;
/** Strip all HTML while rendering. */
noHtml?: boolean;
- /** Strip all HTML, except HTML generated by matchers, while rendering. */
- noHtmlExceptMatchers?: boolean;
- /** Transformer ran on each HTML element. Return a new element, null to remove current element, or undefined to do nothing. */
- transform?: TransformCallback | null;
- /** Disable transformer for non-allowList tags. */
- transformOnlyAllowList?: boolean;
+ /** Strip all HTML, except HTML generated by matchers or transformers, while rendering. */
+ noHtmlExceptInternals?: boolean;
+ /** The element to parse content in. Applies browser semantic rules. */
+ tagName: TagName;
}
// INTERWEAVE
export interface MarkupProps extends ParserProps {
- /** HTML attributes to pass to the wrapping element. */
- attributes?: Attributes;
- /** CSS class name to pass to the wrapping element. */
- className?: string;
/** Content that may contain HTML to safely render. */
content?: string | null;
/** Content to render when the `content` prop is empty. */
emptyContent?: React.ReactNode;
/** @ignore Pre-parsed content to render. */
parsedContent?: React.ReactNode;
- /** HTML element to wrap the content. Also accepts 'fragment' (superseded by `noWrap`). */
- tagName?: string;
- /** Don't wrap the content in a new element specified by `tagName`. */
- noWrap?: boolean;
}
-export interface InterweaveProps extends MarkupProps {
- /** Support all the props used by matchers. */
- [prop: string]: any;
- /** Disable all filters from running. */
- disableFilters?: boolean;
- /** Disable all matches from running. */
- disableMatchers?: boolean;
- /** List of filters to apply to the content. */
- filters?: FilterInterface[];
+export interface InterweaveProps extends MarkupProps {
+ /** List of transformers to apply to elements. */
+ transformers?: Transformer[];
/** List of matchers to apply to the content. */
- matchers?: MatcherInterface[];
+ matchers?: Matcher<{}, TODO>[];
/** Callback fired after parsing ends. Must return an array of React nodes. */
- onAfterParse?: AfterParseCallback | null;
+ onAfterParse?: OnAfterParse | null;
/** Callback fired beore parsing begins. Must return a string. */
- onBeforeParse?: BeforeParseCallback | null;
+ onBeforeParse?: OnAfterParse | null;
}
export interface ElementProps {
From ddb813f2bdd26bd4f068529a9cf9bd278246d4f0 Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Fri, 11 Mar 2022 18:17:33 -0800
Subject: [PATCH 05/18] Update interweave.
---
packages/core/src/Interweave.tsx | 120 +++++++++++++++----------------
packages/core/src/Markup.tsx | 9 +--
packages/core/src/Parser.ts | 36 +++++-----
packages/core/src/types.ts | 16 ++---
4 files changed, 89 insertions(+), 92 deletions(-)
diff --git a/packages/core/src/Interweave.tsx b/packages/core/src/Interweave.tsx
index 5a6ec65e..4514e224 100644
--- a/packages/core/src/Interweave.tsx
+++ b/packages/core/src/Interweave.tsx
@@ -1,78 +1,72 @@
-/* eslint-disable promise/prefer-await-to-callbacks */
-import React from 'react';
-import { Markup } from './Markup';
+import React, { useMemo } from 'react';
import { Parser } from './Parser';
-import { InterweaveProps } from './types';
-
-export function Interweave(props: InterweaveProps) {
- const {
- attributes,
- className,
- content = '',
- disableFilters = false,
- disableMatchers = false,
- emptyContent = null,
- filters = [],
- matchers = [],
- onAfterParse = null,
- onBeforeParse = null,
- tagName = 'span',
- noWrap = false,
- ...parserProps
- } = props;
- const allMatchers = disableMatchers ? [] : matchers;
- const allFilters = disableFilters ? [] : filters;
- const beforeCallbacks = onBeforeParse ? [onBeforeParse] : [];
- const afterCallbacks = onAfterParse ? [onAfterParse] : [];
-
- // Inherit callbacks from matchers
- allMatchers.forEach((matcher) => {
- if (matcher.onBeforeParse) {
- beforeCallbacks.push(matcher.onBeforeParse.bind(matcher));
+import { CommonInternals, InterweaveProps, OnAfterParse, OnBeforeParse } from './types';
+
+export function Interweave(props: InterweaveProps) {
+ const { content, emptyContent, matchers, onAfterParse, onBeforeParse, transformers } = props;
+
+ const mainContent = useMemo(() => {
+ const beforeCallbacks: OnBeforeParse[] = [];
+ const afterCallbacks: OnAfterParse[] = [];
+
+ // Inherit all callbacks
+ function inheritCallbacks(internals: CommonInternals[]) {
+ internals.forEach((internal) => {
+ if (internal.onBeforeParse) {
+ beforeCallbacks.push(internal.onBeforeParse);
+ }
+
+ if (internal.onAfterParse) {
+ afterCallbacks.push(internal.onAfterParse);
+ }
+ });
+ }
+
+ if (matchers) {
+ inheritCallbacks(matchers);
}
- if (matcher.onAfterParse) {
- afterCallbacks.push(matcher.onAfterParse.bind(matcher));
+ if (transformers) {
+ inheritCallbacks(transformers);
}
- });
- // Trigger before callbacks
- const markup = beforeCallbacks.reduce((string, callback) => {
- const nextString = callback(string, props);
+ if (onBeforeParse) {
+ beforeCallbacks.push(onBeforeParse);
+ }
- if (__DEV__ && typeof nextString !== 'string') {
- throw new TypeError('Interweave `onBeforeParse` must return a valid HTML string.');
+ if (onAfterParse) {
+ afterCallbacks.push(onAfterParse);
}
- return nextString;
- }, content ?? '');
+ // Trigger before callbacks
+ const markup = beforeCallbacks.reduce((string, before) => {
+ const nextString = before(string, props as unknown as Props);
+
+ if (__DEV__ && typeof nextString !== 'string') {
+ throw new TypeError('Interweave `onBeforeParse` must return a valid HTML string.');
+ }
- // Parse the markup
- const parser = new Parser(markup, parserProps, allMatchers, allFilters);
+ return nextString;
+ }, content ?? '');
- // Trigger after callbacks
- const nodes = afterCallbacks.reduce((parserNodes, callback) => {
- const nextNodes = callback(parserNodes, props);
+ // Parse the markup
+ const parser = new Parser(markup, props, matchers, transformers);
+ let nodes = parser.parse();
- if (__DEV__ && !Array.isArray(nextNodes)) {
- throw new TypeError(
- 'Interweave `onAfterParse` must return an array of strings and React elements.',
+ // Trigger after callbacks
+ if (nodes) {
+ nodes = afterCallbacks.reduce(
+ (parserNodes, after) => after(parserNodes, props as unknown as Props),
+ nodes,
);
}
- return nextNodes;
- }, parser.parse());
-
- return (
-
- );
+ return nodes;
+
+ // Do not include `props` as we only want to re-render on content changes
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [content, matchers, transformers, onBeforeParse, onAfterParse]);
+
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>{mainContent ?? emptyContent}>;
}
diff --git a/packages/core/src/Markup.tsx b/packages/core/src/Markup.tsx
index aef3cda2..486d0cc6 100644
--- a/packages/core/src/Markup.tsx
+++ b/packages/core/src/Markup.tsx
@@ -3,12 +3,13 @@ import { Parser } from './Parser';
import { MarkupProps } from './types';
export function Markup(props: MarkupProps) {
- const { content, emptyContent, parsedContent } = props;
+ const { content, emptyContent } = props;
+
const mainContent = useMemo(
- () => parsedContent ?? new Parser(content ?? '', props).parse(),
- // Do not include `peops` as we only want to re-render on content changes
+ () => new Parser(content ?? '', props).parse(),
+ // Do not include `props` as we only want to re-render on content changes
// eslint-disable-next-line react-hooks/exhaustive-deps
- [parsedContent, content],
+ [content],
);
// eslint-disable-next-line react/jsx-no-useless-fragment
diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts
index 73c190ba..9fd06b4b 100644
--- a/packages/core/src/Parser.ts
+++ b/packages/core/src/Parser.ts
@@ -28,9 +28,9 @@ import {
TagName,
} from './types';
-type TransformerInterface = Transformer;
+type TransformerInterface = Transformer;
-type MatcherInterface = Matcher;
+type MatcherInterface = Matcher<{}, Props>;
type MatchedElements = Record<
string,
@@ -55,7 +55,7 @@ function createDocument() {
return document.implementation.createHTMLDocument('Interweave');
}
-export class Parser {
+export class Parser {
allowed: Set;
banned: Set;
@@ -70,15 +70,15 @@ export class Parser {
props: ParserProps;
- matchers: MatcherInterface[];
+ matchers: MatcherInterface[];
- transformers: TransformerInterface[];
+ transformers: TransformerInterface[];
constructor(
markup: string,
props: ParserProps,
- matchers: MatcherInterface[] = [],
- transformers: TransformerInterface[] = [],
+ matchers: MatcherInterface[] = [],
+ transformers: TransformerInterface[] = [],
) {
if (__DEV__ && markup && typeof markup !== 'string') {
throw new TypeError('Interweave parser requires a valid string.');
@@ -86,7 +86,8 @@ export class Parser {
this.props = props;
this.matchers = matchers;
- this.transformers = [...transformers, styleTransformer];
+ this.transformers = transformers;
+ this.transformers.push(styleTransformer as unknown as TransformerInterface);
this.keyIndex = -1;
this.container = this.createContainer(markup || '');
this.allowed = new Set(props.allow ?? (ALLOWED_TAG_LIST as TagName[]));
@@ -122,7 +123,10 @@ export class Parser {
// Continuously trigger the matcher until no matches are found
let tokenizedString = '';
- while (matchedString && (parts = matcher.match(matchedString, this.props))) {
+ while (
+ matchedString &&
+ (parts = matcher.match(matchedString, this.props as unknown as Props))
+ ) {
const { index, length, match, valid, void: isVoid, params } = parts;
const tokenName = tagName + String(elementIndex);
@@ -140,7 +144,7 @@ export class Parser {
elementIndex += 1;
elements[tokenName] = {
- element: matcher.factory(params, this.props, match),
+ element: matcher.factory(params, this.props as unknown as Props, match),
key: this.keyIndex,
};
} else {
@@ -185,7 +189,7 @@ export class Parser {
);
for (const transformer of transformers) {
- const result = transformer.factory(node, this.props, children);
+ const result = transformer.factory(node, this.props as unknown as Props, children);
// If something was returned, the node has been replaced so we cant continue
if (result !== undefined) {
@@ -270,8 +274,8 @@ export class Parser {
return undefined;
}
- const tag = this.props.containerTagName ?? 'body';
- const el = tag === 'body' || tag === 'fragment' ? doc.body : doc.createElement(tag);
+ const tag = this.props.tagName ?? 'body';
+ const el = tag === 'body' ? doc.body : doc.createElement(tag);
if (markup.match(INVALID_ROOTS)) {
if (__DEV__) {
@@ -454,7 +458,7 @@ export class Parser {
* list of text nodes and React elements.
*/
parseNode(parentNode: HTMLElement, parentConfig: TagConfig): Node[] {
- const { noHtml, noHtmlExceptMatchers, allowElements } = this.props;
+ const { noHtml, noHtmlExceptInternals, allowElements } = this.props;
let content: Node[] = [];
let mergedText = '';
@@ -512,7 +516,7 @@ export class Parser {
// - Tag is allowed
// - Child is valid within the parent
if (
- !(noHtml || (noHtmlExceptMatchers && tagName !== 'br')) &&
+ !(noHtml || (noHtmlExceptInternals && tagName !== 'br')) &&
this.isTagAllowed(tagName) &&
(allowElements || this.canRenderChild(parentConfig, config))
) {
@@ -553,7 +557,7 @@ export class Parser {
// Apply matchers if a text node
} else if (node.nodeType === TEXT_NODE) {
const text =
- noHtml && !noHtmlExceptMatchers
+ noHtml && !noHtmlExceptInternals
? node.textContent
: // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
this.applyMatchers(node.textContent || '', parentConfig);
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 08887c50..b629f9a0 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -76,7 +76,7 @@ export interface ParserProps {
/** Strip all HTML, except HTML generated by matchers or transformers, while rendering. */
noHtmlExceptInternals?: boolean;
/** The element to parse content in. Applies browser semantic rules. */
- tagName: TagName;
+ tagName?: TagName;
}
// INTERWEAVE
@@ -86,19 +86,17 @@ export interface MarkupProps extends ParserProps {
content?: string | null;
/** Content to render when the `content` prop is empty. */
emptyContent?: React.ReactNode;
- /** @ignore Pre-parsed content to render. */
- parsedContent?: React.ReactNode;
}
-export interface InterweaveProps extends MarkupProps {
+export interface InterweaveProps extends MarkupProps {
/** List of transformers to apply to elements. */
- transformers?: Transformer[];
+ transformers?: Transformer[];
/** List of matchers to apply to the content. */
- matchers?: Matcher<{}, TODO>[];
- /** Callback fired after parsing ends. Must return an array of React nodes. */
- onAfterParse?: OnAfterParse | null;
+ matchers?: Matcher<{}, Props>[];
+ /** Callback fired after parsing ends. Must return a React node. */
+ onAfterParse?: OnAfterParse;
/** Callback fired beore parsing begins. Must return a string. */
- onBeforeParse?: OnAfterParse | null;
+ onBeforeParse?: OnBeforeParse;
}
export interface ElementProps {
From 43a5b376ea6d7a6339ed61830a773efdc48943c9 Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Fri, 11 Mar 2022 18:29:05 -0800
Subject: [PATCH 06/18] Move types around.
---
packages/core/src/Element.tsx | 11 +-
packages/core/src/Interweave.tsx | 16 ++-
packages/core/src/Markup.tsx | 10 +-
packages/core/src/Parser.ts | 41 +++---
packages/core/src/constants.ts | 4 +-
packages/core/src/test.tsx | 217 ++++++++++++-------------------
packages/core/src/types.ts | 68 +---------
7 files changed, 149 insertions(+), 218 deletions(-)
diff --git a/packages/core/src/Element.tsx b/packages/core/src/Element.tsx
index d19c3124..b3085e40 100644
--- a/packages/core/src/Element.tsx
+++ b/packages/core/src/Element.tsx
@@ -1,5 +1,14 @@
import React from 'react';
-import { ElementProps } from './types';
+import { Attributes } from './types';
+
+export interface ElementProps {
+ [prop: string]: unknown;
+ attributes?: Attributes;
+ className?: string;
+ children?: React.ReactNode;
+ selfClose?: boolean;
+ tagName: string;
+}
export function Element({
attributes = {},
diff --git a/packages/core/src/Interweave.tsx b/packages/core/src/Interweave.tsx
index 4514e224..e7e8bcdf 100644
--- a/packages/core/src/Interweave.tsx
+++ b/packages/core/src/Interweave.tsx
@@ -1,6 +1,20 @@
import React, { useMemo } from 'react';
+import type { Matcher } from './createMatcher';
+import type { Transformer } from './createTransformer';
+import { MarkupProps } from './Markup';
import { Parser } from './Parser';
-import { CommonInternals, InterweaveProps, OnAfterParse, OnBeforeParse } from './types';
+import { CommonInternals, OnAfterParse, OnBeforeParse } from './types';
+
+export interface InterweaveProps extends MarkupProps {
+ /** List of transformers to apply to elements. */
+ transformers?: Transformer[];
+ /** List of matchers to apply to the content. */
+ matchers?: Matcher<{}, Props>[];
+ /** Callback fired after parsing ends. Must return a React node. */
+ onAfterParse?: OnAfterParse;
+ /** Callback fired beore parsing begins. Must return a string. */
+ onBeforeParse?: OnBeforeParse;
+}
export function Interweave(props: InterweaveProps) {
const { content, emptyContent, matchers, onAfterParse, onBeforeParse, transformers } = props;
diff --git a/packages/core/src/Markup.tsx b/packages/core/src/Markup.tsx
index 486d0cc6..90520c7f 100644
--- a/packages/core/src/Markup.tsx
+++ b/packages/core/src/Markup.tsx
@@ -1,6 +1,12 @@
import React, { useMemo } from 'react';
-import { Parser } from './Parser';
-import { MarkupProps } from './types';
+import { Parser, ParserProps } from './Parser';
+
+export interface MarkupProps extends ParserProps {
+ /** Content that may contain HTML to safely render. */
+ content?: string | null;
+ /** Content to render when the `content` prop is empty. */
+ emptyContent?: React.ReactNode;
+}
export function Markup(props: MarkupProps) {
const { content, emptyContent } = props;
diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts
index 9fd06b4b..711a0649 100644
--- a/packages/core/src/Parser.ts
+++ b/packages/core/src/Parser.ts
@@ -15,18 +15,9 @@ import {
} from './constants';
import { Matcher } from './createMatcher';
import { Transformer } from './createTransformer';
-import { Element } from './Element';
+import { Element, ElementProps } from './Element';
import { styleTransformer } from './transformers';
-import {
- Attributes,
- AttributeValue,
- ChildrenNode,
- ElementProps,
- Node,
- ParserProps,
- TagConfig,
- TagName,
-} from './types';
+import { Attributes, AttributeValue, Node, TagConfig, TagName } from './types';
type TransformerInterface = Transformer;
@@ -55,6 +46,27 @@ function createDocument() {
return document.implementation.createHTMLDocument('Interweave');
}
+export interface ParserProps {
+ /** Allow all non-banned HTML attributes. */
+ allowAttributes?: boolean;
+ /** Allow all non-banned and non-blocked HTML elements to be rendered. */
+ allowElements?: boolean;
+ /** List of HTML tag names to allow and render. Defaults to the `ALLOWED_TAG_LIST` constant. */
+ allow?: TagName[];
+ /** List of HTML tag names to disallow and not render. Overrides allow list. */
+ block?: TagName[];
+ /** Disable the conversion of new lines to ` ` elements. */
+ disableLineBreaks?: boolean;
+ /** Escape all HTML before parsing. */
+ escapeHtml?: boolean;
+ /** Strip all HTML while rendering. */
+ noHtml?: boolean;
+ /** Strip all HTML, except HTML generated by matchers or transformers, while rendering. */
+ noHtmlExceptInternals?: boolean;
+ /** The element to parse content in. Applies browser semantic rules. */
+ tagName?: TagName;
+}
+
export class Parser {
allowed: Set;
@@ -100,7 +112,7 @@ export class Parser {
* If a match is found, create a React element, and build a new array.
* This array allows React to interpolate and render accordingly.
*/
- applyMatchers(string: string, parentConfig: TagConfig): ChildrenNode {
+ applyMatchers(string: string, parentConfig: TagConfig): Node {
const elements: MatchedElements = {};
let matchedString = string;
let elementIndex = 0;
@@ -433,7 +445,6 @@ export class Parser {
return false;
}
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return this.props.allowElements || this.allowed.has(tagName);
}
@@ -565,7 +576,7 @@ export class Parser {
if (Array.isArray(text)) {
content = [...content, ...text];
} else {
- mergedText += text!;
+ mergedText += text;
}
}
});
@@ -581,7 +592,7 @@ export class Parser {
* Deconstruct the string into an array, by replacing custom tokens with React elements,
* so that React can render it correctly.
*/
- replaceTokens(tokenizedString: string, elements: MatchedElements): ChildrenNode {
+ replaceTokens(tokenizedString: string, elements: MatchedElements): Node {
if (!tokenizedString.includes('{{{')) {
return tokenizedString;
}
diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts
index abf50c7f..43847ca9 100644
--- a/packages/core/src/constants.ts
+++ b/packages/core/src/constants.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-bitwise, no-magic-numbers, sort-keys */
-import { FilterMap, TagConfig, TagConfigMap } from './types';
+import { TagConfig, TagConfigMap } from './types';
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories
export const TYPE_FLOW = 1;
@@ -303,7 +303,7 @@ export const FILTER_NO_CAST = 5;
// Attributes not listed here will be denied
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
-export const ATTRIBUTES: FilterMap = Object.freeze({
+export const ATTRIBUTES: Record = Object.freeze({
alt: FILTER_ALLOW,
cite: FILTER_ALLOW,
class: FILTER_ALLOW,
diff --git a/packages/core/src/test.tsx b/packages/core/src/test.tsx
index 1a3deed2..3374eb44 100644
--- a/packages/core/src/test.tsx
+++ b/packages/core/src/test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { ChildrenNode, Element, Filter, Matcher, MatchResponse, Node, TagConfig, TAGS } from '.';
+import { createMatcher, createTransformer, Element, TagConfig, TAGS } from '.';
export const TOKEN_LOCATIONS = [
'no tokens',
@@ -24,28 +24,28 @@ export const TOKEN_LOCATIONS = [
export const SOURCE_PROP = {
compact: false,
locale: 'en',
- version: '0.0.0',
-} as const;
+ version: 'latest',
+};
export const VALID_EMOJIS = [
- ['1F621', '😡', ':rage:', '>:/'],
+ ['1F621', '😡', ':enraged:', '>:/'],
['1F468-200D-1F469-200D-1F467-200D-1F466', '👨👩👧👦', ':family_mwgb:'],
['1F1FA-1F1F8', '🇺🇸', ':flag_us:'],
- ['1F63A', '😺', ':grinning_cat:'],
+ ['1F63A', '😺', ':smiling_cat:'],
['1F3EF', '🏯', ':japanese_castle:'],
['1F681', '🚁', ':helicopter:'],
- ['1F469-200D-2764-FE0F-200D-1F468', '👩❤️👨', ':couple_with_heart_mw:'],
- ['1F1E7-1F1F4', '🇧🇴', ':bolivia:'],
+ ['1F469-200D-2764-FE0F-200D-1F468', '👩❤️👨', ':couple_mw:'],
+ ['1F1E7-1F1F4', '🇧🇴', ':flag_bo:'],
['1F468-200D-1F468-200D-1F466', '👨👨👦', ':family_mmb:'],
['1F3C0', '🏀', ':basketball:'],
];
export function createExpectedToken(
value: T,
- factory: (val: T, count: number) => React.ReactNode,
+ factory: (value: T, count: number) => React.ReactNode,
index: number,
join: boolean = false,
-): React.ReactNode | string {
+): React.ReactNode {
if (index === 0) {
return TOKEN_LOCATIONS[0];
}
@@ -96,126 +96,79 @@ export const parentConfig: TagConfig = {
...TAGS.div,
};
-export function matchCodeTag(
- string: string,
- tag: string,
-): MatchResponse<{
- children: string;
- customProp: string;
-}> | null {
- const matches = string.match(new RegExp(`\\[${tag}\\]`));
-
- if (!matches) {
- return null;
- }
-
- return {
- children: tag,
- customProp: 'foo',
- index: matches.index!,
- length: matches[0].length,
- match: matches[0],
- valid: true,
- void: false,
- };
-}
-
-export class CodeTagMatcher extends Matcher<{}> {
- tag: string;
-
- key: string;
-
- constructor(tag: string, key: string = '') {
- super(tag, {});
-
- this.tag = tag;
- this.key = key;
- }
-
- replaceWith(match: ChildrenNode, props: { children?: string; key?: string } = {}): Node {
- const { children } = props;
-
- if (this.key) {
- // eslint-disable-next-line no-param-reassign
- props.key = this.key;
- }
-
- return (
-
- {children!.toUpperCase()}
-
- );
- }
-
- asTag() {
- return 'span';
- }
-
- match(string: string) {
- return matchCodeTag(string, this.tag);
- }
-}
-
-export class MarkdownBoldMatcher extends Matcher {
- replaceWith(children: ChildrenNode, props: object): Node {
- return {children} ;
- }
-
- asTag() {
- return 'b';
- }
-
- match(value: string) {
- return this.doMatch(value, /\*\*([^*]+)\*\*/u, (matches) => ({ match: matches[1] }));
- }
-}
-
-export class MarkdownItalicMatcher extends Matcher {
- replaceWith(children: ChildrenNode, props: object): Node {
- return {children} ;
- }
-
- asTag() {
- return 'i';
- }
-
- match(value: string) {
- return this.doMatch(value, /_([^_]+)_/u, (matches) => ({ match: matches[1] }));
- }
-}
-
-export class MockMatcher extends Matcher {
- replaceWith(children: ChildrenNode, props: any): Node {
- return {children}
;
- }
-
- asTag() {
- return 'div';
- }
-
- match() {
- return null;
- }
-}
-
-export class LinkFilter extends Filter {
- override attribute(name: string, value: string): string {
- if (name === 'href') {
- return value.replace('foo.com', 'bar.net');
- }
-
- return value;
- }
-
- override node(name: string, node: HTMLElement): HTMLElement | null {
- if (name === 'a') {
- node.setAttribute('target', '_blank');
- } else if (name === 'link') {
- return null;
- }
-
- return node;
- }
-}
-
-export class MockFilter extends Filter {}
+export const codeFooMatcher = createMatcher(
+ /\[foo]/,
+ (match, props, children) => {String(children).toUpperCase()} ,
+ {
+ onMatch: () => ({
+ codeTag: 'foo',
+ customProp: 'foo',
+ }),
+ tagName: 'span',
+ },
+);
+
+export const codeBarMatcher = createMatcher(
+ /\[bar]/,
+ (match, props, children) => {String(children).toUpperCase()} ,
+ {
+ onMatch: () => ({
+ codeTag: 'bar',
+ customProp: 'bar',
+ }),
+ tagName: 'span',
+ },
+);
+
+export const codeBazMatcher = createMatcher(
+ /\[baz]/,
+ (match, props, children) => {String(children).toUpperCase()} ,
+ {
+ onMatch: () => ({
+ codeTag: 'baz',
+ customProp: 'baz',
+ }),
+ tagName: 'span',
+ },
+);
+
+export const mdBoldMatcher = createMatcher(
+ /\*\*([^*]+)\*\*/u,
+ (match, props, children) => {children} ,
+ {
+ onMatch: ({ matches }) => ({
+ match: matches[1],
+ }),
+ tagName: 'b',
+ },
+);
+
+export const mdItalicMatcher = createMatcher(
+ /_([^_]+)_/u,
+ (match, props, children) => {children} ,
+ {
+ onMatch: ({ matches }) => ({
+ match: matches[1],
+ }),
+ tagName: 'i',
+ },
+);
+
+export const mockMatcher = createMatcher(
+ /div/,
+ (match, props, children) => {children}
,
+ {
+ onMatch: () => null,
+ tagName: 'div',
+ },
+);
+
+export const linkTransformer = createTransformer('a', (element) => {
+ element.setAttribute('target', '_blank');
+
+ if (element.href) {
+ element.setAttribute('href', element.href.replace('foo.com', 'bar.net') || '');
+ }
+});
+
+export const mockTransformer = createTransformer('*', () => {});
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index b629f9a0..b8fbd788 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -1,8 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-
import React from 'react';
-import type { Matcher } from './createMatcher';
-import type { Transformer } from './createTransformer';
declare global {
// eslint-disable-next-line no-var, vars-on-top
@@ -15,8 +11,6 @@ export interface CommonInternals {
options: Partial;
}
-// ELEMENTS
-
export type TagName = keyof React.ReactHTML | 'rb' | 'rtc';
export type WildTagName = TagName | '*';
@@ -42,68 +36,12 @@ export interface TagConfig {
export type TagConfigMap = Record>;
-// CALLBACKS
-
-export type OnAfterParse = (content: Node, props: Props) => Node;
-
-export type OnBeforeParse = (content: string, props: Props) => string;
-
-export type Node = NonNullable;
-
-export type ChildrenNode = Node[] | string;
-
export type AttributeValue = boolean | number | object | string;
export type Attributes = Record;
-// PARSER
-
-export interface ParserProps {
- /** Allow all non-banned HTML attributes. */
- allowAttributes?: boolean;
- /** Allow all non-banned and non-blocked HTML elements to be rendered. */
- allowElements?: boolean;
- /** List of HTML tag names to allow and render. Defaults to the `ALLOWED_TAG_LIST` constant. */
- allow?: TagName[];
- /** List of HTML tag names to disallow and not render. Overrides allow list. */
- block?: TagName[];
- /** Disable the conversion of new lines to ` ` elements. */
- disableLineBreaks?: boolean;
- /** Escape all HTML before parsing. */
- escapeHtml?: boolean;
- /** Strip all HTML while rendering. */
- noHtml?: boolean;
- /** Strip all HTML, except HTML generated by matchers or transformers, while rendering. */
- noHtmlExceptInternals?: boolean;
- /** The element to parse content in. Applies browser semantic rules. */
- tagName?: TagName;
-}
-
-// INTERWEAVE
-
-export interface MarkupProps extends ParserProps {
- /** Content that may contain HTML to safely render. */
- content?: string | null;
- /** Content to render when the `content` prop is empty. */
- emptyContent?: React.ReactNode;
-}
+export type Node = NonNullable;
-export interface InterweaveProps extends MarkupProps {
- /** List of transformers to apply to elements. */
- transformers?: Transformer[];
- /** List of matchers to apply to the content. */
- matchers?: Matcher<{}, Props>[];
- /** Callback fired after parsing ends. Must return a React node. */
- onAfterParse?: OnAfterParse;
- /** Callback fired beore parsing begins. Must return a string. */
- onBeforeParse?: OnBeforeParse;
-}
+export type OnAfterParse = (content: Node, props: Props) => Node;
-export interface ElementProps {
- [prop: string]: any;
- attributes?: Attributes;
- className?: string;
- children?: React.ReactNode;
- selfClose?: boolean;
- tagName: string;
-}
+export type OnBeforeParse = (content: string, props: Props) => string;
From fc8698d020a36e03908c0da03f8ec066a25a9106 Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Fri, 11 Mar 2022 21:49:02 -0800
Subject: [PATCH 07/18] Update tests.
---
packages/core/tests/Element.test.tsx | 3 +-
packages/core/tests/Filter.test.ts | 18 -
packages/core/tests/HTML.test.tsx | 7 +-
packages/core/tests/Markup.test.tsx | 25 +-
packages/core/tests/Parser.test.tsx | 70 +-
.../tests/__snapshots__/HTML.test.tsx.snap | 2364 +++++++----------
6 files changed, 992 insertions(+), 1495 deletions(-)
delete mode 100644 packages/core/tests/Filter.test.ts
diff --git a/packages/core/tests/Element.test.tsx b/packages/core/tests/Element.test.tsx
index 64fa3095..85cafc23 100644
--- a/packages/core/tests/Element.test.tsx
+++ b/packages/core/tests/Element.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { render } from 'rut-dom';
-import { Element } from '../src/Element';
-import { ElementProps } from '../src/types';
+import { Element, ElementProps } from '../src/Element';
describe('Element', () => {
it('renders with a custom HTML tag', () => {
diff --git a/packages/core/tests/Filter.test.ts b/packages/core/tests/Filter.test.ts
deleted file mode 100644
index 1cc776da..00000000
--- a/packages/core/tests/Filter.test.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Filter } from '../src/Filter';
-import { LinkFilter } from '../src/test';
-
-describe('Filter', () => {
- it('runs the filter', () => {
- expect(new LinkFilter().attribute('href', 'foo.com')).toBe('bar.net');
- });
-
- it('returns attribute value by default', () => {
- expect(new Filter().attribute('href', 'foo.com')).toBe('foo.com');
- });
-
- it('returns node by default', () => {
- const a = document.createElement('a');
-
- expect(new Filter().node('a', a)).toBe(a);
- });
-});
diff --git a/packages/core/tests/HTML.test.tsx b/packages/core/tests/HTML.test.tsx
index 6d51acb5..4b936d9e 100644
--- a/packages/core/tests/HTML.test.tsx
+++ b/packages/core/tests/HTML.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { render } from 'rut-dom';
-import { Markup } from '../src/Markup';
-import { MarkupProps } from '../src/types';
+import { Markup, MarkupProps } from '../src/Markup';
// All examples taken from MDN https://developer.mozilla.org/en-US/docs/Web/HTML/Element
describe('html', () => {
@@ -187,7 +186,7 @@ describe('html', () => {
it('renders', () => {
const result = render(
An alternative text describing what your canvas displays.
`}
@@ -455,7 +454,7 @@ describe('html', () => {
it('renders', () => {
const result = render(
`}
/>,
);
diff --git a/packages/core/tests/Markup.test.tsx b/packages/core/tests/Markup.test.tsx
index fbd404c8..055cb3d3 100644
--- a/packages/core/tests/Markup.test.tsx
+++ b/packages/core/tests/Markup.test.tsx
@@ -1,9 +1,8 @@
import React from 'react';
import { render } from 'rut-dom';
import { Element } from '../src/Element';
-import { Markup } from '../src/Markup';
+import { Markup, MarkupProps } from '../src/Markup';
import { MOCK_MARKUP } from '../src/test';
-import { MarkupProps } from '../src/types';
const options = { log: false, reactElements: false };
@@ -14,28 +13,6 @@ describe('Markup', () => {
expect(root.findOne(Element)).toHaveProp('tagName', 'p');
});
- it('can use a fragment', () => {
- const { root } = render( );
-
- expect(root).toContainNode('Foo Bar');
- });
-
- it('can pass custom attributes', () => {
- const { root } = render(
- Bar Baz'} />,
- );
-
- expect(root.findOne('span')).toHaveProp('aria-label', 'foo');
- });
-
- it('can pass class name', () => {
- const { root } = render(
- Bar Baz'} />,
- );
-
- expect(root.findOne('span')).toHaveProp('className', 'foo');
- });
-
it('allows empty `content` to be passed', () => {
const { root } = render( );
diff --git a/packages/core/tests/Parser.test.tsx b/packages/core/tests/Parser.test.tsx
index e90d9ba0..a439786d 100644
--- a/packages/core/tests/Parser.test.tsx
+++ b/packages/core/tests/Parser.test.tsx
@@ -9,9 +9,10 @@ import {
import { Element } from '../src/Element';
import { Parser } from '../src/Parser';
import {
- CodeTagMatcher,
+ codeBarMatcher,
+ codeBazMatcher,
+ codeFooMatcher,
createExpectedToken,
- LinkFilter,
MOCK_MARKUP,
parentConfig,
TOKEN_LOCATIONS,
@@ -25,15 +26,15 @@ function createChild(tag: string, text: number | string): HTMLElement {
}
describe('Parser', () => {
- let instance: Parser;
+ let instance: Parser<{}>;
let element: HTMLElement;
beforeEach(() => {
instance = new Parser(
'',
- {},
- [new CodeTagMatcher('foo'), new CodeTagMatcher('bar'), new CodeTagMatcher('baz')],
- [new LinkFilter()],
+ { tagName: 'div' },
+ [codeFooMatcher, codeBarMatcher, codeBazMatcher],
+ [], // [new LinkFilter()],
);
});
@@ -51,25 +52,6 @@ describe('Parser', () => {
});
});
- describe('applyAttributeFilters()', () => {
- it('applies filters for the attribute name', () => {
- expect(instance.applyAttributeFilters('src', 'foo.com')).toBe('foo.com');
- expect(instance.applyAttributeFilters('href', 'foo.com')).toBe('bar.net');
- });
- });
-
- describe('applyNodeFilters()', () => {
- it('applies filters to the node', () => {
- const a = document.createElement('a');
-
- expect(a.getAttribute('target')).toBeNull();
-
- instance.applyNodeFilters('a', a);
-
- expect(a.getAttribute('target')).toBe('_blank');
- });
- });
-
describe('applyMatchers()', () => {
function createElement(value: string, key: number) {
return (
@@ -158,15 +140,29 @@ describe('Parser', () => {
describe('canRenderChild()', () => {
it('doesnt render if missing parent tag', () => {
- expect(instance.canRenderChild({ ...parentConfig, tagName: '' }, { ...parentConfig })).toBe(
- false,
- );
+ expect(
+ instance.canRenderChild(
+ {
+ ...parentConfig,
+ // @ts-expect-error Invalid
+ tagName: '',
+ },
+ { ...parentConfig },
+ ),
+ ).toBe(false);
});
it('doesnt render if missing child tag', () => {
- expect(instance.canRenderChild({ ...parentConfig }, { ...parentConfig, tagName: '' })).toBe(
- false,
- );
+ expect(
+ instance.canRenderChild(
+ { ...parentConfig },
+ {
+ ...parentConfig,
+ // @ts-expect-error Invalid
+ tagName: '',
+ },
+ ),
+ ).toBe(false);
});
});
@@ -467,8 +463,10 @@ describe('Parser', () => {
});
it('returns true if in allow list', () => {
+ // @ts-expect-error Invalid
instance.allowed.add('custom');
+ // @ts-expect-error Invalid
expect(instance.isTagAllowed('custom')).toBe(true);
});
@@ -487,7 +485,7 @@ describe('Parser', () => {
describe('parse()', () => {
it('parses the entire document starting from the body', () => {
- instance = new Parser(MOCK_MARKUP);
+ instance = new Parser(MOCK_MARKUP, {});
expect(instance.parse()).toMatchSnapshot();
});
@@ -592,9 +590,9 @@ describe('Parser', () => {
]);
});
- it('passes through elements if `noHtmlExceptMatchers` prop is set', () => {
+ it('passes through elements if `noHtmlExceptInternals` prop is set', () => {
instance = new Parser('', {
- noHtmlExceptMatchers: true,
+ noHtmlExceptInternals: true,
});
element.append(document.createTextNode('Foo'));
@@ -622,8 +620,8 @@ describe('Parser', () => {
expect(instance.parseNode(element, parentConfig)).toEqual(['Foo', 'Bar', '[foo]']);
});
- it('doesnt strip matchers HTML if `noHtmlExceptMatchers` prop is set', () => {
- instance.props.noHtmlExceptMatchers = true;
+ it('doesnt strip matchers HTML if `noHtmlExceptInternals` prop is set', () => {
+ instance.props.noHtmlExceptInternals = true;
element.append(document.createTextNode('Foo'));
element.append(createChild('div', 'Bar'));
diff --git a/packages/core/tests/__snapshots__/HTML.test.tsx.snap b/packages/core/tests/__snapshots__/HTML.test.tsx.snap
index 1a1d9547..f8655fe6 100644
--- a/packages/core/tests/__snapshots__/HTML.test.tsx.snap
+++ b/packages/core/tests/__snapshots__/HTML.test.tsx.snap
@@ -8,79 +8,63 @@ exports[`html a, ul, li renders 1`] = `
Phone
"
>
-
-
-
-
+
+
+
`;
exports[`html abbr renders 1`] = `
-CSS to style your HTML ."
->
-
-
- You can use
-
-
- CSS
-
-
- to style your
-
-
- HTML
-
-
- .
-
+CSS to style your HTML .">
+ You can use
+
+
+ CSS
+
+
+ to style your
+
+
+ HTML
+
+ .
`;
@@ -91,35 +75,31 @@ exports[`html address renders 1`] = `
+311-555-2368
"
>
-
-
-
-
-
+
+
+
-
-
- jim@rock.com
-
-
-
-
-
-
+
+
+ jim@rock.com
+
+
+
+
+
+
-
-
- +311-555-2368
-
-
-
-
-
-
-
-
+
+
+ +311-555-2368
+
-
+
+
+
+
+
+
`;
@@ -142,74 +122,70 @@ exports[`html article renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
- Weather forecast for Seattle
-
-
+
+ Weather forecast for Seattle
+
+
-
-
-
+
+
+
-
- 03 March 2018
-
-
+
+ 03 March 2018
+
+
-
- Rain.
-
-
-
-
+
+ Rain.
-
-
-
+
+
+
+
+
+
+
-
- 04 March 2018
-
-
+
+ 04 March 2018
+
+
-
- Periods of rain.
-
-
-
-
+
+ Periods of rain.
-
-
-
+
+
+
+
+
+
+
-
- 05 March 2018
-
-
+
+ 05 March 2018
+
+
-
- Heavy rain.
-
-
-
-
+
+ Heavy rain.
-
+
-
+
+
+
`;
@@ -220,20 +196,16 @@ exports[`html aside renders 1`] = `
The Rough-skinned Newt defends itself with a deadly neurotoxin.
"
>
-
-
-
-
-
+
+
+
+ The Rough-skinned Newt defends itself with a deadly neurotoxin.
-
+
+
+
`;
@@ -244,21 +216,14 @@ exports[`html audio renders 1`] = `
Your browser does not support the audio
element.
"
>
-
-
-
-
-
+
+
+
Your browser does not support the
- audio
- element.
+ audio
+ element.
-
-
-
+
`;
@@ -270,27 +235,19 @@ exports[`html audio renders with source 1`] = `
Your browser does not support the audio
element.
"
>
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
Your browser does not support the
- audio
- element.
+ audio
+ element.
-
-
-
+
`;
@@ -299,26 +256,22 @@ exports[`html b renders 1`] = `
chemistry (the study of chemicals and the composition of substances) and physics (the study of the nature and properties of matter and energy).
"
>
-
-
-
-
- The two most popular science courses offered by the school are
-
-
- chemistry
-
-
- (the study of chemicals and the composition of substances) and
-
-
- physics
-
-
- (the study of the nature and properties of matter and energy).
-
+
+
+ The two most popular science courses offered by the school are
+
+
+ chemistry
+
-
+ (the study of chemicals and the composition of substances) and
+
+
+ physics
+
+
+ (the study of the nature and properties of matter and energy).
+
`;
@@ -333,95 +286,82 @@ exports[`html bdi renders 1`] = `
تیز سمی : 5th place
"
>
-
-
-
-
-
+
+
+
-
-
-
-
- Evil Steven
-
-
- : 1st place
-
+
+
+
+
+ Evil Steven
+
-
+ : 1st place
+
+
+
-
-
-
-
- François fatale
-
-
- : 2nd place
-
+
+
+
+
+ François fatale
+
-
+ : 2nd place
+
+
+
-
-
-
-
- تیز سمی
-
-
- : 3rd place
-
+
+
+
+
+ تیز سمی
+
-
+ : 3rd place
+
+
+
-
-
-
-
- الرجل القوي إيان
-
-
- : 4th place
-
+
+
+
+
+ الرجل القوي إيان
+
-
+ : 4th place
+
+
+
-
-
-
-
- تیز سمی
-
-
- : 5th place
-
+
+
+
+
+ تیز سمی
+
-
-
-
+ : 5th place
+
-
+
+
+
`;
exports[`html bdo renders 1`] = `
-אה, אני אוהב להיות ליד חוף הים"
->
-
-
- In the computer's memory, this is stored as
-
-
- אה, אני אוהב להיות ליד חוף הים
-
-
-
+אה, אני אוהב להיות ליד חוף הים">
+ In the computer's memory, this is stored as
+
+
+ אה, אני אוהב להיות ליד חוף הים
+
`;
@@ -434,65 +374,47 @@ exports[`html blockquote renders 1`] = `
– Aldous Huxley, Brave New World "
>
-
-
-
-
-
+
+
+
-
- Words can be like X-rays, if you use them properly – they'll go through anything. You read and you're pierced.
-
-
-
-
+
+ Words can be like X-rays, if you use them properly – they'll go through anything. You read and you're pierced.
+
+
+
-
- – Aldous Huxley, Brave New World
-
-
+
+
+ – Aldous Huxley, Brave New World
`;
exports[`html button renders 1`] = `
Click me">
-
-
-
- Click me
-
-
+
+ Click me
`;
exports[`html canvas renders 1`] = `
An alternative text describing what your canvas displays.
"
>
-
-
-
-
-
+
+
+
An alternative text describing what your canvas displays.
-
-
-
+
`;
@@ -503,11 +425,9 @@ exports[`html canvas renders nothing when not allowed 1`] = `
An alternative text describing what your canvas displays.
"
>
-
-
+
An alternative text describing what your canvas displays.
-
-
+
`;
@@ -522,57 +442,47 @@ exports[`html caption renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
- He-Man and Skeletor facts
-
-
+
+ He-Man and Skeletor facts
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
- He-Man
-
-
-
+
+
+ He-Man
+
+
+
-
-
- Skeletor
-
-
-
-
-
+
+
+ Skeletor
+
-
-
+
+
-
+
+
+
-
+
`;
@@ -586,64 +496,51 @@ exports[`html cite renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
- It was a bright cold day in April, and the clocks were striking thirteen.
-
-
+
+ It was a bright cold day in April, and the clocks were striking thirteen.
+
+
-
-
+ by George Orwell (Part 1, Chapter 1).
+
+
-
+
+
+
`;
exports[`html code renders 1`] = `
-
-
-
-
-
- The
-
- push()
-
- method adds one or more elements to the end of an array and returns the new length of the array.
-
+
+
+
+ The
+
+ push()
-
+ method adds one or more elements to the end of an array and returns the new length of the array.
+
`;
@@ -659,60 +556,46 @@ exports[`html col, colgroup renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
- Superheros and sidekicks
-
-
+
+ Superheros and sidekicks
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
-
-
+
+
-
+
+
+
`;
exports[`html details, summary doesnt render summary when not in details 1`] = `
-
- Details
-
+ Details
`;
@@ -723,43 +606,33 @@ exports[`html details, summary renders 1`] = `
Something small enough to escape casual notice.
"
>
-
-
-
-
-
+
+
+
-
- Details
-
-
+
+ Details
+
+
Something small enough to escape casual notice.
-
-
-
+
`;
exports[`html dfn renders 1`] = `
-validator is a program that checks for syntax errors in code or documents."
->
-
-
-
-
- A
-
-
- validator
-
-
- is a program that checks for syntax errors in code or documents.
-
+validator is a program that checks for syntax errors in code or documents.">
+
+
+ A
+
+
+ validator
+
-
+ is a program that checks for syntax errors in code or documents.
+
`;
@@ -771,32 +644,21 @@ exports[`html div renders 1`] = `
Beware of the leopard
"
>
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
- Beware of the leopard
-
-
-
-
+
+ Beware of the leopard
-
+
+
+
`;
@@ -814,62 +676,54 @@ exports[`html dl, dt, dd renders 1`] = `
A giant owl-like creature.
"
>
-
-
-
-
-
+
+
+
-
- Beast of Bodmin
-
-
+
+ Beast of Bodmin
+
+
-
- A large feline inhabiting Bodmin Moor.
-
-
+
+ A large feline inhabiting Bodmin Moor.
+
+
-
- Morgawr
-
-
+
+ Morgawr
+
+
-
- A sea serpent.
-
-
+
+ A sea serpent.
+
+
-
- Owlman
-
-
+
+ Owlman
+
+
-
- A giant owl-like creature.
-
-
-
-
+
+ A giant owl-like creature.
-
+
+
+
`;
exports[`html em renders 1`] = `
-
-
- We
-
- had
-
- to do something about it.
-
+ We
+
+ had
+ to do something about it.
`;
@@ -880,32 +734,21 @@ exports[`html figure, figcaption renders 1`] = `
An elephant at sunset
"
>
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
- An elephant at sunset
-
-
-
-
+
+ An elephant at sunset
-
+
+
+
`;
@@ -924,56 +767,52 @@ exports[`html footer renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
- How to be a wizard
-
-
+
+ How to be a wizard
+
+
-
-
-
+
+
+
-
- Grow a long, majestic beard.
-
-
+
+ Grow a long, majestic beard.
+
+
-
- Wear a tall pointed hat.
-
-
+
+ Wear a tall pointed hat.
+
+
-
- Have I mentioned the beard?
-
-
-
-
+
+ Have I mentioned the beard?
-
-
-
-
-
- © 2018 Gandalf
-
-
+
+
+
-
+
+
+
+
+
+ © 2018 Gandalf
-
-
+
+
-
+
+
+
`;
@@ -988,54 +827,50 @@ exports[`html h1 - h6 renders 1`] = `
Prothorax
Pterothorax "
>
-
-
-
- Beetles
-
-
-
-
+
+ Beetles
+
+
+
+
+
+
+ External morphology
+
+
+
+
-
- External morphology
-
-
-
-
+
+ Head
+
+
+
+
-
- Head
-
-
-
-
-
-
- Mouthparts
-
-
-
-
+
+ Mouthparts
+
+
+
+
+
+
+ Thorax
+
+
+
+
-
- Thorax
-
-
-
-
-
-
- Prothorax
-
-
-
-
-
-
- Pterothorax
-
-
+
+ Prothorax
+
+
+
+
+
+
+ Pterothorax
`;
@@ -1046,20 +881,16 @@ exports[`html header renders 1`] = `
Cute Puppies Express!
"
>
-
-
-
-
-
+
+
+
-
- Cute Puppies Express!
-
-
-
-
+
+ Cute Puppies Express!
-
+
+
+
`;
@@ -1070,103 +901,57 @@ exports[`html hr renders 1`] = `
§2: The second rule of Fight Club is: Always bring cupcakes.
"
>
-
-
-
- §1: The first rule of Fight Club is: You do not talk about Fight Club.
-
-
+
+ §1: The first rule of Fight Club is: You do not talk about Fight Club.
+
+
-
-
-
-
+
+
+
+
-
- §2: The second rule of Fight Club is: Always bring cupcakes.
-
-
+
+ §2: The second rule of Fight Club is: Always bring cupcakes.
`;
exports[`html i renders 1`] = `
-Musa is one of two or three genera in the family Musaceae ; it includes bananas and plantains."
->
-
-
-
-
-
-
- Musa
-
-
- is one of two or three genera in the family
-
-
- Musaceae
-
-
- ; it includes bananas and plantains.
-
+Musa is one of two or three genera in the family Musaceae ; it includes bananas and plantains.">
+
+
+
+
+ Musa
+
-
+ is one of two or three genera in the family
+
+
+ Musaceae
+
+
+ ; it includes bananas and plantains.
+
`;
exports[`html iframe renders 1`] = `
-"
->
-
-
-
-
-
-
+">
+
+
`;
-exports[`html iframe renders nothing when not allowed 1`] = `
-"
->
-
-
-
-
-`;
+exports[`html iframe renders nothing when not allowed 1`] = `" />`;
exports[`html img renders 1`] = `
- "
->
-
-
-
-
-
-
+ ">
+
+
`;
@@ -1177,46 +962,33 @@ exports[`html ins renders 1`] = `
“A wizard is never late …”
"
>
-
-
-
-
-
+
+
+
- “A wizard is never late …”
-
+ “A wizard is never late …”
+
-
-
-
+
`;
exports[`html kbd renders 1`] = `
-
-
-
- Please press
-
- Ctrl
-
- +
-
- Shift
-
- +
-
- R
-
- to re-render an MDN page.
-
+
+ Please press
+
+ Ctrl
+ +
+
+ Shift
+
+ +
+
+ R
+
+ to re-render an MDN page.
`;
@@ -1226,37 +998,27 @@ exports[`html main renders 1`] = `
Geckos are a group of usually small, usually nocturnal lizards. They are found on every continent except Australia.
"
>
-
-
-
-
-
+
+
+
-
- Geckos are a group of usually small, usually nocturnal lizards. They are found on every continent except Australia.
-
-
-
-
+
+ Geckos are a group of usually small, usually nocturnal lizards. They are found on every continent except Australia.
-
+
+
+
`;
exports[`html mark renders 1`] = `
-
-
-
- Most
-
- salamander
-
- s are nocturnal, and hunt for insects, worms, and other small creatures.
-
+
+ Most
+
+ salamander
+ s are nocturnal, and hunt for insects, worms, and other small creatures.
`;
@@ -1270,52 +1032,48 @@ exports[`html nav, ol, li renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
- Bikes
-
-
-
+
+
+
+
+ Bikes
+
-
+
+
+
-
-
-
-
- BMX
-
-
-
+
+
+
+
+ BMX
+
-
+
+
+
-
-
- Jump Bike 3000
-
-
-
-
-
+
+
+ Jump Bike 3000
+
-
-
+
+
-
+
+
+
`;
@@ -1327,36 +1085,21 @@ exports[`html picture renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
-
+
+
+
`;
@@ -1382,10 +1125,8 @@ exports[`html pre renders 1`] = `
RER - Apollinaire
"
>
-
-
-
- L TE
+
+ L TE
A A
C V
R A
@@ -1402,28 +1143,17 @@ exports[`html pre renders 1`] = `
SI RESPI
RER - Apollinaire
-
-
`;
exports[`html q renders 1`] = `
-I'm sorry, Dave. I'm afraid I can't do that."
->
-
-
- When Dave asks HAL to open the pod bay door, HAL answers:
-
-
- I'm sorry, Dave. I'm afraid I can't do that.
-
-
-
+I'm sorry, Dave. I'm afraid I can't do that.">
+ When Dave asks HAL to open the pod bay door, HAL answers:
+
+
+ I'm sorry, Dave. I'm afraid I can't do that.
+
`;
@@ -1442,132 +1172,80 @@ exports[`html ruby, rp, rt, rtc renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
+
-
- 馬
-
-
- (
-
-
- mǎ
-
-
- )
-
-
+ 馬
+ (
+ mǎ
+ )
+
-
- 來
-
-
- (
-
-
- lái
-
-
- )
-
-
+ 來
+ (
+ lái
+ )
+
+
+ 西
+ (
+ xī
+ )
+
-
- 西
-
-
- (
-
-
- xī
-
-
- )
-
+ 亞
+ (
+ yà
+ )
+
+
+
+
+
+
-
- 亞
-
(
- yà
+ Malaysia
)
-
-
-
-
-
-
-
- (
-
-
- Malaysia
-
-
- )
-
-
-
-
-
-
-
-
+
-
+
+
+
`;
exports[`html s renders 1`] = `
-
-
-
-
- There will be a few tickets available at the box office tonight.
-
-
+
+
+ There will be a few tickets available at the box office tonight.
`;
exports[`html samp renders 1`] = `
-
-
-
-
- Keyboard not found
-
-
-
- Press F1 to continue
-
+
+
+ Keyboard not found
+
+
-
+ Press F1 to continue
+
`;
@@ -1579,54 +1257,38 @@ exports[`html section renders 1`] = `
People have been catching fish for food since before recorded history...
"
>
-
-
-
-
-
+
+
+
-
- Introduction
-
-
+
+ Introduction
+
+
-
- People have been catching fish for food since before recorded history...
-
-
-
-
+
+ People have been catching fish for food since before recorded history...
-
+
+
+
`;
exports[`html small renders 1`] = `
-
-
-
-
-
-
- The content is licensed under a Creative Commons Attribution-ShareAlike 2.5 Generic License.
-
-
+
+
+
+
+ The content is licensed under a Creative Commons Attribution-ShareAlike 2.5 Generic License.
-
+
`;
-exports[`html source doesnt render if outside its parent 1`] = `
-">
-
-
-
-
-`;
+exports[`html source doesnt render if outside its parent 1`] = `" />`;
exports[`html source renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
This browser does not support the HTML5 video element.
-
-
-
+
`;
exports[`html span renders 1`] = `
-basil , pine nuts and garlic to a blender and blend into a paste."
->
-
-
- Add the
-
-
- basil
-
-
- ,
-
-
- pine nuts
-
-
- and
-
-
- garlic
-
-
- to a blender and blend into a paste.
+basil , pine nuts and garlic to a blender and blend into a paste.">
+ Add the
+
+
+ basil
+
+
+ ,
+
+
+ pine nuts
+
+
+ and
+
+
+ garlic
+ to a blender and blend into a paste.
`;
exports[`html strong renders 1`] = `
-
-
-
- ... the most important rule, the rule you can never forget, no matter how much he cries, no matter how much he begs:
-
- never feed him after midnight
-
- .
-
+
+ ... the most important rule, the rule you can never forget, no matter how much he cries, no matter how much he begs:
+
+ never feed him after midnight
+ .
`;
@@ -1722,64 +1357,54 @@ exports[`html sub renders 1`] = `
content="Almost every developer's favorite molecule is
C8 H10 N4 O2 , also known as "caffeine.""
>
-
-
- Almost every developer's favorite molecule is
+ Almost every developer's favorite molecule is
C
-
- 8
-
- H
-
- 10
-
- N
-
- 4
-
- O
-
- 2
-
- , also known as "caffeine."
-
+
+ 8
+ H
+
+ 10
+
+ N
+
+ 4
+
+ O
+
+ 2
+
+ , also known as "caffeine."
`;
exports[`html sup renders 1`] = `
-
-
-
-
-
- a
-
- 2
-
-
-
- +
-
-
- b
-
- 2
-
-
-
- =
-
-
- c
-
- 2
-
-
+
+
+
+ a
+
+ 2
-
+
+
+ +
+
+
+ b
+
+ 2
+
+
+
+ =
+
+
+ c
+
+ 2
+
+
`;
@@ -1811,119 +1436,115 @@ exports[`html table, thead, tbody, tfoot renders 1`] = `
"
>
-
-
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
- Items
-
-
+
+ Items
+
+
-
-
- Expenditure
-
-
-
-
-
+
+
+ Expenditure
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
- Donuts
-
-
-
+
+
-
- 3,000
-
-
+
+
+
+
-
+
+
+
+
+
+
+ Donuts
+
-
-
-
-
-
-
-
- Stationary
-
-
-
-
- 18,000
-
-
-
-
+
+ 3,000
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+ Stationary
+
+
+
+
+
+ 18,000
+
-
-
-
-
-
-
- Totals
-
-
-
+
+
+
+
+
+
-
- 21,000
-
-
+
+
+
+
-
+
+
+
+
+
+
+ Totals
+
-
-
+
+
+ 21,000
+
+
+
+
-
-
+
+
-
+
+
+
`;
@@ -1941,59 +1562,55 @@ exports[`html table, thead, tbody, tfoot renders table with direct rows 1`] = `
"
>
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
- Donuts
-
-
-
+
+
+ Donuts
+
+
+
-
- 3,000
-
-
-
-
+
+ 3,000
-
-
-
+
+
+
+
+
+
+
-
-
- Stationary
-
-
-
+
+
+ Stationary
+
+
+
-
- 18,000
-
-
-
-
+
+ 18,000
-
-
+
+
-
+
+
+
-
+
`;
@@ -2007,22 +1624,18 @@ exports[`html tbody doesnt render outside of table 1`] = `
"
>
-
-
+
Donuts
3,000
-
-
+
`;
exports[`html td doesnt render outside of tr 1`] = `
-
- 3,000
-
+ 3,000
`;
@@ -2035,22 +1648,18 @@ exports[`html tfoot doesnt render outside of table 1`] = `
"
>
-
-
+
Totals
21,000
-
-
+
`;
exports[`html th doesnt render outside of tr 1`] = `
Donuts">
-
- Donuts
-
+ Donuts
`;
@@ -2063,32 +1672,24 @@ exports[`html thead doesnt render outside of table 1`] = `
"
>
-
-
+
Items
Expenditure
-
-
+
`;
exports[`html time renders 1`] = `
-July 7 in London's Hyde Park."
->
-
-
- The Cure will be celebrating their 40th anniversary on
-
-
- July 7
-
-
- in London's Hyde Park.
-
+July 7 in London's Hyde Park.">
+ The Cure will be celebrating their 40th anniversary on
+
+
+ July 7
+
+ in London's Hyde Park.
`;
@@ -2099,25 +1700,15 @@ exports[`html tr doesnt render outside of table 1`] = `
3,000
"
>
-
-
+
Donuts
3,000
-
-
-
-`;
-exports[`html track doesnt render if outside its parent 1`] = `
- "
->
-
-
-
`;
+exports[`html track doesnt render if outside its parent 1`] = ` " />`;
+
exports[`html track renders 1`] = `
@@ -2126,100 +1717,67 @@ exports[`html track renders 1`] = `
Sorry, your browser doesn't support embedded videos.
"
>
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
Sorry, your browser doesn't support embedded videos.
-
-
-
+
`;
exports[`html u renders 1`] = `
-
-
-
-
-
- You could use this element to highlight
-
- speling
-
- mistakes, so the writer can
-
- corect
-
- them.
-
+
+
+
+ You could use this element to highlight
+
+ speling
-
+ mistakes, so the writer can
+
+ corect
+
+ them.
+
`;
exports[`html var renders 1`] = `
-
-
-
- The volume of a box is
-
- l
-
- ×
-
- w
-
- ×
-
- h
-
- , where
-
- l
-
- represents the length,
-
- w
-
- the width and
-
- h
-
- the height of the box.
-
+
+ The volume of a box is
+
+ l
+
+ ×
+
+ w
+
+ ×
+
+ h
+ , where
+
+ l
+
+ represents the length,
+
+ w
+
+ the width and
+
+ h
+
+ the height of the box.
`;
@@ -2231,68 +1789,52 @@ exports[`html video renders 1`] = `
Your browser doesn't support HTML5 video. Here is a link to the video instead.
"
>
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
- Your browser doesn't support HTML5 video. Here is a
-
-
- link to the video
-
-
- instead.
-
-
-
+ Your browser doesn't support HTML5 video. Here is a
+
+
+ link to the video
+
-
+ instead.
+
+
+
`;
exports[`html wbr renders 1`] = `
-
-
- Fernstraßen
-
-
-
- bau
-
-
-
- privat
-
-
-
- finanzierungs
-
-
-
- gesetz
-
+ Fernstraßen
+
+
+
+ bau
+
+
+
+ privat
+
+
+
+ finanzierungs
+
+
+ gesetz
`;
From 5e6bd61eda9ce7e94455cb0918dfeac5d190f39b Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Sat, 12 Mar 2022 12:22:56 -0800
Subject: [PATCH 08/18] Fix parser tests.
---
packages/core/src/Parser.ts | 23 ++++---
packages/core/src/createMatcher.ts | 14 ++--
packages/core/src/test.tsx | 24 +++++--
packages/core/src/types.ts | 2 +-
packages/core/tests/Parser.test.tsx | 66 ++++---------------
.../tests/__snapshots__/Parser.test.tsx.snap | 15 +++++
6 files changed, 70 insertions(+), 74 deletions(-)
diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts
index 711a0649..51aabfc0 100644
--- a/packages/core/src/Parser.ts
+++ b/packages/core/src/Parser.ts
@@ -88,7 +88,7 @@ export class Parser {
constructor(
markup: string,
- props: ParserProps,
+ props: ParserProps = {},
matchers: MatcherInterface[] = [],
transformers: TransformerInterface[] = [],
) {
@@ -185,7 +185,13 @@ export class Parser {
return string;
}
- return this.replaceTokens(matchedString, elements);
+ // console.log('applyMatchers', matchedString, ...Object.values(elements));
+
+ const a = this.replaceTokens(matchedString, elements);
+
+ // console.log(a);
+
+ return a;
}
/**
@@ -390,7 +396,7 @@ export class Parser {
invalid: [],
parent: [],
self: true,
- tagName,
+ tagName: null,
type: 0,
void: false,
};
@@ -486,9 +492,6 @@ export class Parser {
mergedText = '';
}
- // Increase key before transforming
- this.keyIndex += 1;
-
// Must occur after key is set
const key = this.keyIndex;
const children = this.parseNode(node as HTMLElement, config);
@@ -574,7 +577,7 @@ export class Parser {
this.applyMatchers(node.textContent || '', parentConfig);
if (Array.isArray(text)) {
- content = [...content, ...text];
+ content.push(...text);
} else {
mergedText += text;
}
@@ -622,6 +625,8 @@ export class Parser {
const { element, key } = elements[tokenName];
let endIndex: number;
+ // console.log('replaceTokens', text, { match, tokenName, startIndex, isVoid, key }, element);
+
// Use tag as-is if void
if (isVoid) {
endIndex = match.length;
@@ -642,7 +647,8 @@ export class Parser {
React.cloneElement(
element,
{ key },
- this.replaceTokens(text.slice(match.length, close.index), elements),
+ element.props.children ??
+ this.replaceTokens(text.slice(match.length, close.index), elements),
),
);
}
@@ -660,6 +666,7 @@ export class Parser {
if (nodes.length === 0) {
return '';
}
+
if (nodes.length === 1 && typeof nodes[0] === 'string') {
return nodes[0];
}
diff --git a/packages/core/src/createMatcher.ts b/packages/core/src/createMatcher.ts
index 58a0b7b7..5949a032 100644
--- a/packages/core/src/createMatcher.ts
+++ b/packages/core/src/createMatcher.ts
@@ -50,15 +50,19 @@ export interface Matcher extends CommonInternals(
pattern: RegExp | string,
- factory: MatcherFactory,
options: MatcherOptions,
+ factory: MatcherFactory,
): Matcher {
return {
extend(customFactory, customOptions) {
- return createMatcher(pattern, customFactory ?? factory, {
- ...options,
- ...customOptions,
- });
+ return createMatcher(
+ pattern,
+ {
+ ...options,
+ ...customOptions,
+ },
+ customFactory ?? factory,
+ );
},
factory,
greedy: options.greedy ?? false,
diff --git a/packages/core/src/test.tsx b/packages/core/src/test.tsx
index 3374eb44..5577ea3f 100644
--- a/packages/core/src/test.tsx
+++ b/packages/core/src/test.tsx
@@ -98,7 +98,6 @@ export const parentConfig: TagConfig = {
export const codeFooMatcher = createMatcher(
/\[foo]/,
- (match, props, children) => {String(children).toUpperCase()} ,
{
onMatch: () => ({
codeTag: 'foo',
@@ -106,11 +105,15 @@ export const codeFooMatcher = createMatcher(
}),
tagName: 'span',
},
+ (match, props, c) => (
+
+ {match.codeTag.toUpperCase()}
+
+ ),
);
export const codeBarMatcher = createMatcher(
/\[bar]/,
- (match, props, children) => {String(children).toUpperCase()} ,
{
onMatch: () => ({
codeTag: 'bar',
@@ -118,11 +121,15 @@ export const codeBarMatcher = createMatcher(
}),
tagName: 'span',
},
+ (match) => (
+
+ {match.codeTag.toUpperCase()}
+
+ ),
);
export const codeBazMatcher = createMatcher(
/\[baz]/,
- (match, props, children) => {String(children).toUpperCase()} ,
{
onMatch: () => ({
codeTag: 'baz',
@@ -130,37 +137,42 @@ export const codeBazMatcher = createMatcher(
}),
tagName: 'span',
},
+ (match) => (
+
+ {match.codeTag.toUpperCase()}
+
+ ),
);
export const mdBoldMatcher = createMatcher(
/\*\*([^*]+)\*\*/u,
- (match, props, children) => {children} ,
{
onMatch: ({ matches }) => ({
match: matches[1],
}),
tagName: 'b',
},
+ (match, props, children) => {children} ,
);
export const mdItalicMatcher = createMatcher(
/_([^_]+)_/u,
- (match, props, children) => {children} ,
{
onMatch: ({ matches }) => ({
match: matches[1],
}),
tagName: 'i',
},
+ (match, props, children) => {children} ,
);
export const mockMatcher = createMatcher(
/div/,
- (match, props, children) => {children}
,
{
onMatch: () => null,
tagName: 'div',
},
+ (match, props, children) => {children}
,
);
export const linkTransformer = createTransformer('a', (element) => {
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index b8fbd788..5fee903f 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -27,7 +27,7 @@ export interface TagConfig {
// Can render self as a child
self: boolean;
// HTML tag name
- tagName: TagName;
+ tagName: TagName | null;
// Self content type
type: number;
// Self-closing tag
diff --git a/packages/core/tests/Parser.test.tsx b/packages/core/tests/Parser.test.tsx
index a439786d..1a13b689 100644
--- a/packages/core/tests/Parser.test.tsx
+++ b/packages/core/tests/Parser.test.tsx
@@ -13,6 +13,7 @@ import {
codeBazMatcher,
codeFooMatcher,
createExpectedToken,
+ linkTransformer,
MOCK_MARKUP,
parentConfig,
TOKEN_LOCATIONS,
@@ -32,9 +33,9 @@ describe('Parser', () => {
beforeEach(() => {
instance = new Parser(
'',
- { tagName: 'div' },
+ {},
[codeFooMatcher, codeBarMatcher, codeBazMatcher],
- [], // [new LinkFilter()],
+ [linkTransformer],
);
});
@@ -55,7 +56,7 @@ describe('Parser', () => {
describe('applyMatchers()', () => {
function createElement(value: string, key: number) {
return (
-
+
{value.toUpperCase()}
);
@@ -120,22 +121,6 @@ describe('Parser', () => {
});
});
});
-
- describe('ignores matcher if the inverse prop is enabled', () => {
- TOKEN_LOCATIONS.forEach((location) => {
- it(`for: ${location}`, () => {
- // @ts-expect-error Invalid type
- instance.props.noFoo = true;
-
- const tokenString = location.replace(/{token}/g, '[foo]');
- const actual = instance.applyMatchers(tokenString, parentConfig);
-
- expect(actual).toBe(tokenString);
-
- instance.props = {};
- });
- });
- });
});
describe('canRenderChild()', () => {
@@ -209,43 +194,41 @@ describe('Parser', () => {
});
describe('convertLineBreaks()', () => {
- /* eslint-disable jest/valid-title */
-
- it('it doesnt convert when HTML closing tags exist', () => {
+ it('doesnt convert when HTML closing tags exist', () => {
expect(instance.convertLineBreaks('It\nwont\r\nconvert.
')).toBe(
'It\nwont\r\nconvert.
',
);
});
- it('it doesnt convert when HTML void tags exist', () => {
+ it('doesnt convert when HTML void tags exist', () => {
expect(instance.convertLineBreaks('It\n wont\r\nconvert.')).toBe(
'It\n wont\r\nconvert.',
);
});
- it('it doesnt convert when HTML void tags with spaces exist', () => {
+ it('doesnt convert when HTML void tags with spaces exist', () => {
expect(instance.convertLineBreaks('It\n wont\r\nconvert.')).toBe(
'It\n wont\r\nconvert.',
);
});
- it('it doesnt convert if `noHtml` is true', () => {
+ it('doesnt convert if `noHtml` is true', () => {
instance.props.noHtml = true;
expect(instance.convertLineBreaks('It\nwont\r\nconvert.')).toBe('It\nwont\r\nconvert.');
});
- it('it doesnt convert if `disableLineBreaks` is true', () => {
+ it('doesnt convert if `disableLineBreaks` is true', () => {
instance.props.disableLineBreaks = true;
expect(instance.convertLineBreaks('It\nwont\r\nconvert.')).toBe('It\nwont\r\nconvert.');
});
- it('it replaces carriage returns', () => {
+ it('replaces carriage returns', () => {
expect(instance.convertLineBreaks('Foo\r\nBar')).toBe('Foo Bar');
});
- it('it replaces super long multilines', () => {
+ it('replaces super long multilines', () => {
expect(instance.convertLineBreaks('Foo\n\n\n\n\n\n\nBar')).toBe('Foo Bar');
});
});
@@ -405,14 +388,6 @@ describe('Parser', () => {
});
});
- it('applies filters to attributes', () => {
- element.setAttribute('href', 'http://foo.com/hello/world');
-
- expect(instance.extractAttributes(element)).toEqual({
- href: 'http://bar.net/hello/world',
- });
- });
-
it('converts `style` to an object', () => {
element.setAttribute('style', 'background-color: red; color: black; display: inline-block;');
@@ -424,19 +399,6 @@ describe('Parser', () => {
},
});
});
-
- it('removes problematic values from `style`', () => {
- element.setAttribute(
- 'style',
- 'color: blue; background-image: url("foo.png"); background: image(ltr "arrow.png#xywh=0,0,16,16", red); border: image-set("cat.jpg" 1x, "dog.jpg" 1x)',
- );
-
- expect(instance.extractAttributes(element)).toEqual({
- style: {
- color: 'blue',
- },
- });
- });
});
describe('isTagAllowed()', () => {
@@ -771,11 +733,7 @@ describe('Parser', () => {
element.append(acronym);
- expect(instance.parseNode(element, instance.getTagConfig('span'))).toEqual([
-
- {['Link']}
- ,
- ]);
+ expect(instance.parseNode(element, instance.getTagConfig('span'))).toMatchSnapshot();
});
});
});
diff --git a/packages/core/tests/__snapshots__/Parser.test.tsx.snap b/packages/core/tests/__snapshots__/Parser.test.tsx.snap
index 9b8811cf..33fe8048 100644
--- a/packages/core/tests/__snapshots__/Parser.test.tsx.snap
+++ b/packages/core/tests/__snapshots__/Parser.test.tsx.snap
@@ -113,3 +113,18 @@ Array [
,
]
`;
+
+exports[`Parser parseNode() uses parent config for unsupported elements 1`] = `
+Array [
+
+ Link
+ ,
+]
+`;
From ea2deed6846f52d660502dd95a4f8d18fe10de95 Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Sat, 12 Mar 2022 14:38:27 -0800
Subject: [PATCH 09/18] Fix element issues.
---
packages/core/src/Element.tsx | 2 +-
packages/core/src/Parser.ts | 65 ++-
packages/core/src/createMatcher.ts | 54 +-
packages/core/src/createTransformer.ts | 16 +-
packages/core/src/test.tsx | 44 +-
packages/core/src/transformers.ts | 2 +-
packages/core/tests/Interweave.test.tsx | 232 +++-----
packages/core/tests/Markup.test.tsx | 9 +-
packages/core/tests/Matcher.test.tsx | 97 ----
.../__snapshots__/Interweave.test.tsx.snap | 494 ++++++++----------
.../tests/__snapshots__/Markup.test.tsx.snap | 64 ++-
11 files changed, 426 insertions(+), 653 deletions(-)
delete mode 100644 packages/core/tests/Matcher.test.tsx
diff --git a/packages/core/src/Element.tsx b/packages/core/src/Element.tsx
index b3085e40..2ce445d1 100644
--- a/packages/core/src/Element.tsx
+++ b/packages/core/src/Element.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { memo } from 'react';
import { Attributes } from './types';
export interface ElementProps {
diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts
index 51aabfc0..ffd7edcb 100644
--- a/packages/core/src/Parser.ts
+++ b/packages/core/src/Parser.ts
@@ -13,21 +13,22 @@ import {
FILTER_NO_CAST,
TAGS,
} from './constants';
-import { Matcher } from './createMatcher';
+import { Matcher, MatchParams } from './createMatcher';
import { Transformer } from './createTransformer';
import { Element, ElementProps } from './Element';
import { styleTransformer } from './transformers';
import { Attributes, AttributeValue, Node, TagConfig, TagName } from './types';
-type TransformerInterface = Transformer;
+type TransformerInterface = Transformer;
-type MatcherInterface = Matcher<{}, Props>;
+type MatcherInterface = Matcher<{}, Props>;
-type MatchedElements = Record<
+type MatchedElements = Record<
string,
{
- element: React.ReactElement;
key: number;
+ matcher: MatcherInterface;
+ params: MatchParams;
}
>;
@@ -67,7 +68,7 @@ export interface ParserProps {
tagName?: TagName;
}
-export class Parser {
+export class Parser {
allowed: Set;
banned: Set;
@@ -113,7 +114,7 @@ export class Parser {
* This array allows React to interpolate and render accordingly.
*/
applyMatchers(string: string, parentConfig: TagConfig): Node {
- const elements: MatchedElements = {};
+ const elements: MatchedElements = {};
let matchedString = string;
let elementIndex = 0;
let parts = null;
@@ -156,8 +157,9 @@ export class Parser {
elementIndex += 1;
elements[tokenName] = {
- element: matcher.factory(params, this.props as unknown as Props, match),
key: this.keyIndex,
+ matcher,
+ params,
};
} else {
tokenizedString += match;
@@ -185,13 +187,7 @@ export class Parser {
return string;
}
- // console.log('applyMatchers', matchedString, ...Object.values(elements));
-
- const a = this.replaceTokens(matchedString, elements);
-
- // console.log(a);
-
- return a;
+ return this.replaceTokens(matchedString, elements);
}
/**
@@ -200,14 +196,14 @@ export class Parser {
applyTransformers(
tagName: TagName,
node: HTMLElement,
- children: unknown[],
+ children: Node[],
): HTMLElement | React.ReactElement | null | undefined {
const transformers = this.transformers.filter(
(transformer) => transformer.tagName === tagName || transformer.tagName === '*',
);
for (const transformer of transformers) {
- const result = transformer.factory(node, this.props as unknown as Props, children);
+ const result = transformer.factory(node, this.getProps(), children);
// If something was returned, the node has been replaced so we cant continue
if (result !== undefined) {
@@ -386,6 +382,13 @@ export class Parser {
return styles;
}
+ /**
+ * Return current props correctly typed.
+ */
+ getProps(): Props {
+ return this.props as unknown as Props;
+ }
+
/**
* Return configuration for a specific tag.
*/
@@ -595,7 +598,7 @@ export class Parser {
* Deconstruct the string into an array, by replacing custom tokens with React elements,
* so that React can render it correctly.
*/
- replaceTokens(tokenizedString: string, elements: MatchedElements): Node {
+ replaceTokens(tokenizedString: string, elements: MatchedElements): Node {
if (!tokenizedString.includes('{{{')) {
return tokenizedString;
}
@@ -622,16 +625,16 @@ export class Parser {
text = text.slice(startIndex);
}
- const { element, key } = elements[tokenName];
+ const { matcher, params, key } = elements[tokenName];
let endIndex: number;
- // console.log('replaceTokens', text, { match, tokenName, startIndex, isVoid, key }, element);
-
// Use tag as-is if void
if (isVoid) {
endIndex = match.length;
- nodes.push(React.cloneElement(element, { key }));
+ // nodes.push(React.cloneElement(element, { key }));
+
+ nodes.push(matcher.factory(params, this.getProps(), null, key));
// Find the closing tag if not void
} else {
@@ -643,12 +646,20 @@ export class Parser {
endIndex = close.index! + close[0].length;
+ // nodes.push(
+ // React.cloneElement(
+ // element,
+ // { key },
+ // this.replaceTokens(text.slice(match.length, close.index), elements),
+ // ),
+ // );
+
nodes.push(
- React.cloneElement(
- element,
- { key },
- element.props.children ??
- this.replaceTokens(text.slice(match.length, close.index), elements),
+ matcher.factory(
+ params,
+ this.getProps(),
+ this.replaceTokens(text.slice(match.length, close.index), elements),
+ key,
),
);
}
diff --git a/packages/core/src/createMatcher.ts b/packages/core/src/createMatcher.ts
index 5949a032..b6a245de 100644
--- a/packages/core/src/createMatcher.ts
+++ b/packages/core/src/createMatcher.ts
@@ -1,11 +1,6 @@
import { CommonInternals, Node, OnAfterParse, OnBeforeParse, TagName } from './types';
-export type OnMatch = (
- result: MatchResult,
- props: Props,
- options: Partial,
-) => Match | null;
-
+// Result from the match process
export interface MatchResult {
index: number;
length: number;
@@ -16,12 +11,23 @@ export interface MatchResult {
void: boolean;
}
-export type MatchHandler = (
- value: string,
- props: Props,
-) => (MatchResult & { params: Match }) | null;
+// Params returned from `onMatch` that are passed to the factory
+export interface MatchParams {
+ [key: string]: unknown;
+ match?: string;
+}
-export interface MatcherOptions {
+export type OnMatch<
+ Match extends MatchParams,
+ Props extends object,
+ Options extends object = {},
+> = (result: MatchResult, props: Props, options: Partial) => Match | null;
+
+export interface MatcherOptions<
+ Match extends MatchParams,
+ Props extends object,
+ Options extends object = {},
+> {
greedy?: boolean;
tagName: TagName;
void?: boolean;
@@ -31,24 +37,33 @@ export interface MatcherOptions {
onMatch: OnMatch;
}
-export type MatcherFactory = (
- match: Match,
+export type MatcherFactory = (
+ params: Match,
props: Props,
- content: Node,
+ children: Node | null,
+ key: number,
) => React.ReactElement;
-export interface Matcher extends CommonInternals {
+export interface Matcher<
+ Match extends MatchParams,
+ Props extends object,
+ Options extends object = {},
+> extends CommonInternals {
extend: (
factory?: MatcherFactory | null,
options?: Partial>,
) => Matcher;
factory: MatcherFactory;
greedy: boolean;
- match: MatchHandler;
+ match: (value: string, props: Props) => (MatchResult & { params: Match }) | null;
tagName: TagName;
}
-export function createMatcher(
+export function createMatcher<
+ Match extends MatchParams,
+ Props extends object = {},
+ Options extends object = {},
+>(
pattern: RegExp | string,
options: MatcherOptions,
factory: MatcherFactory,
@@ -90,6 +105,11 @@ export function createMatcher(
return null;
}
+ // Allow callback to replace the matched content
+ if ('match' in params && params.match) {
+ result.match = params.match;
+ }
+
return {
params,
...result,
diff --git a/packages/core/src/createTransformer.ts b/packages/core/src/createTransformer.ts
index bc1252d3..154cdf3d 100644
--- a/packages/core/src/createTransformer.ts
+++ b/packages/core/src/createTransformer.ts
@@ -9,7 +9,7 @@ export type InferElement = K extends '*'
export type TransformerFactory = (
element: Element,
props: Props,
- content: Node,
+ children: Node[],
) => Element | React.ReactElement | null | undefined | void;
export interface TransformerOptions {
@@ -30,15 +30,19 @@ export interface Transformer extends CommonInterna
export function createTransformer(
tagName: K,
+ options: TransformerOptions,
factory: TransformerFactory, Props>,
- options: TransformerOptions = {},
): Transformer, Props, Options> {
return {
extend(customFactory, customOptions) {
- return createTransformer(tagName, customFactory ?? factory, {
- ...options,
- ...customOptions,
- });
+ return createTransformer(
+ tagName,
+ {
+ ...options,
+ ...customOptions,
+ },
+ customFactory ?? factory,
+ );
},
factory,
onAfterParse: options.onAfterParse,
diff --git a/packages/core/src/test.tsx b/packages/core/src/test.tsx
index 5577ea3f..8eb83221 100644
--- a/packages/core/src/test.tsx
+++ b/packages/core/src/test.tsx
@@ -100,14 +100,14 @@ export const codeFooMatcher = createMatcher(
/\[foo]/,
{
onMatch: () => ({
- codeTag: 'foo',
+ match: 'foo',
customProp: 'foo',
}),
tagName: 'span',
},
- (match, props, c) => (
-
- {match.codeTag.toUpperCase()}
+ (params, props, children, key) => (
+
+ {children.toUpperCase()}
),
);
@@ -116,14 +116,14 @@ export const codeBarMatcher = createMatcher(
/\[bar]/,
{
onMatch: () => ({
- codeTag: 'bar',
+ match: 'bar',
customProp: 'bar',
}),
tagName: 'span',
},
- (match) => (
-
- {match.codeTag.toUpperCase()}
+ (params, props, children, key) => (
+
+ {children.toUpperCase()}
),
);
@@ -132,14 +132,14 @@ export const codeBazMatcher = createMatcher(
/\[baz]/,
{
onMatch: () => ({
- codeTag: 'baz',
+ match: 'baz',
customProp: 'baz',
}),
tagName: 'span',
},
- (match) => (
-
- {match.codeTag.toUpperCase()}
+ (params, props, children, key) => (
+
+ {children.toUpperCase()}
),
);
@@ -152,7 +152,7 @@ export const mdBoldMatcher = createMatcher(
}),
tagName: 'b',
},
- (match, props, children) => {children} ,
+ (params, props, children, key) => {children} ,
);
export const mdItalicMatcher = createMatcher(
@@ -163,7 +163,7 @@ export const mdItalicMatcher = createMatcher(
}),
tagName: 'i',
},
- (match, props, children) => {children} ,
+ (params, props, children, key) => {children} ,
);
export const mockMatcher = createMatcher(
@@ -172,15 +172,21 @@ export const mockMatcher = createMatcher(
onMatch: () => null,
tagName: 'div',
},
- (match, props, children) => {children}
,
+ (params, props, children, key) => (
+
+ {children}
+
+ ),
);
-export const linkTransformer = createTransformer('a', (element) => {
+export const linkTransformer = createTransformer('a', {}, (element, p, c) => {
element.setAttribute('target', '_blank');
- if (element.href) {
- element.setAttribute('href', element.href.replace('foo.com', 'bar.net') || '');
+ const href = element.getAttribute('href');
+
+ if (href) {
+ element.setAttribute('href', href.replace('foo.com', 'bar.net') || '');
}
});
-export const mockTransformer = createTransformer('*', () => {});
+export const mockTransformer = createTransformer('*', {}, () => {});
diff --git a/packages/core/src/transformers.ts b/packages/core/src/transformers.ts
index 60ebb9fc..a5e8859c 100644
--- a/packages/core/src/transformers.ts
+++ b/packages/core/src/transformers.ts
@@ -2,7 +2,7 @@ import { createTransformer } from './createTransformer';
const INVALID_STYLES = /(url|image|image-set)\(/i;
-export const styleTransformer = createTransformer('*', (element) => {
+export const styleTransformer = createTransformer('*', {}, (element) => {
Object.keys(element.style).forEach((k) => {
const key = k as keyof typeof element.style;
diff --git a/packages/core/tests/Interweave.test.tsx b/packages/core/tests/Interweave.test.tsx
index bb1d58c2..ed303f9f 100644
--- a/packages/core/tests/Interweave.test.tsx
+++ b/packages/core/tests/Interweave.test.tsx
@@ -6,11 +6,12 @@ import { ALLOWED_TAG_LIST } from '../src/constants';
import { Element } from '../src/Element';
import { Interweave } from '../src/Interweave';
import {
- CodeTagMatcher,
- LinkFilter,
- MarkdownBoldMatcher,
- MarkdownItalicMatcher,
- matchCodeTag,
+ codeBarMatcher,
+ codeBazMatcher,
+ codeFooMatcher,
+ linkTransformer,
+ mdBoldMatcher,
+ mdItalicMatcher,
MOCK_INVALID_MARKUP,
MOCK_MARKUP,
} from '../src/test';
@@ -22,108 +23,26 @@ describe('Interweave', () => {
expect(ALLOWED_TAG_LIST).not.toContain('iframe');
});
- it('can pass custom attributes', () => {
+ it('can pass transformers through props', () => {
const { root } = render(
- Bar Baz'}
- />,
+ Bar Baz'} transformers={[linkTransformer]} />,
);
- expect(root.findOne('span')).toHaveProp('aria-label', 'foo');
- });
-
- it('can pass class name', () => {
- const { root } = render(
- Bar Baz'} />,
- );
-
- expect(root.findOne('span')).toHaveProp('className', 'foo');
- });
-
- it('can pass filters through props', () => {
- const { root } = render(
- Bar Baz'} filters={[new LinkFilter()]} />,
- );
-
- expect(root.findAt(Element, 0)).toMatchSnapshot();
- });
-
- it('can pass object based filters through props', () => {
- const { root } = render(
- Bar Baz'}
- filters={[
- {
- attribute: (name, value) =>
- name === 'href' ? value.replace('foo.com', 'bar.net') : value,
- },
- ]}
- />,
- );
-
- expect(root.findAt(Element, 0)).toMatchSnapshot();
- });
-
- it('can disable all filters using `disableFilters`', () => {
- const { root } = render(
- Bar Baz'}
- filters={[new LinkFilter()]}
- />,
- );
-
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('can pass matchers through props', () => {
const { root } = render(
- ,
+ ,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
- });
-
- it('can pass object based matchers through props', () => {
- const { root } = render(
- 'span',
- createElement: (match: ChildrenNode, p: { children: string }) => (
-
- {p.children.toUpperCase()}
-
- ),
- match: (string) => matchCodeTag(string, 'b'),
- },
- ]}
- />,
- );
-
- expect(root.findAt(Element, 0)).toMatchSnapshot();
- });
-
- it('can disable all matchers using `disableMatchers`', () => {
- const { root } = render(
- ,
- );
-
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('allows empty `content` to be passed', () => {
const { root } = render( );
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('allows empty `content` to be passed when using callbacks', () => {
@@ -131,15 +50,15 @@ describe('Interweave', () => {
value} />,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('renders using a custom container element', () => {
const { root } = render(
- ,
+ ,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
describe('parseMarkup()', () => {
@@ -150,13 +69,6 @@ describe('Interweave', () => {
}).toThrowErrorMatchingSnapshot();
});
- it('errors if onAfterParse doesnt return an array', () => {
- expect(() => {
- // @ts-expect-error Invalid type
- render( 123} />);
- }).toThrowErrorMatchingSnapshot();
- });
-
it('can modify the markup using onBeforeParse', () => {
const { root } = render(
{
/>,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('can modify the tree using onAfterParse', () => {
const { root } = render(
Bar Baz'}
- onAfterParse={(content) => {
- content.push(
+ onAfterParse={(content) => (
+ <>
+ {content}
Qux
- ,
- );
-
- return content;
- }}
+
+ >
+ )}
/>,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
- });
- });
-
- describe('render()', () => {
- it('renders with a default tag name', () => {
- const { root } = render( );
-
- expect(root.findAt(Element, 0)).toHaveProp('tagName', 'span');
- });
-
- it('renders with a custom tag name', () => {
- const { root } = render( );
-
- expect(root.findAt(Element, 0)).toHaveProp('tagName', 'div');
- });
-
- it('parses HTML', () => {
- const { root } = render(
- Bar Baz'} tagName="div" />,
- );
-
- expect(root.findAt(Element, 0)).toHaveProp('tagName', 'div');
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
});
@@ -222,7 +110,7 @@ describe('Interweave', () => {
/>,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
});
@@ -230,21 +118,21 @@ describe('Interweave', () => {
it('converts line breaks', () => {
const { root } = render( );
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
- it('converts line breaks if `noHtmlExceptMatchers` is true', () => {
+ it('converts line breaks if `noHtmlExceptInternals` is true', () => {
const { root } = render(
- ,
+ ,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('doesnt convert line breaks if `noHtml` is true', () => {
const { root } = render( );
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('doesnt convert line breaks if `disableLineBreaks` is true', () => {
@@ -252,13 +140,13 @@ describe('Interweave', () => {
,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('doesnt convert line breaks if it contains HTML', () => {
const { root } = render(Bar'} />);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
});
@@ -266,7 +154,7 @@ describe('Interweave', () => {
it('filters invalid tags and attributes', () => {
const { root } = render( );
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('doesnt filter invalid tags and attributes when disabled', () => {
@@ -274,17 +162,17 @@ describe('Interweave', () => {
,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
});
describe('block list', () => {
it('filters blocked tags and attributes', () => {
const { root } = render(
- ,
+ ,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
});
@@ -308,7 +196,7 @@ describe('Interweave', () => {
,
);
- expect(actual).toBe('This is bold . ');
+ expect(actual).toBe('This is bold .');
expect(implSpy).not.toHaveBeenCalled();
});
@@ -317,7 +205,7 @@ describe('Interweave', () => {
,
);
- expect(actual).toBe('This is bold. ');
+ expect(actual).toBe('This is bold.');
expect(implSpy).not.toHaveBeenCalled();
});
@@ -326,25 +214,28 @@ describe('Interweave', () => {
,
);
- expect(actual).toBe('This is bold . ');
+ expect(actual).toBe('This is bold .');
expect(implSpy).not.toHaveBeenCalled();
});
- it('supports filters', () => {
+ it.only('supports transformers', () => {
const actual = ReactDOMServer.renderToStaticMarkup(
- Bar Baz'} filters={[new LinkFilter()]} />,
+ Bar Baz'}
+ transformers={[linkTransformer]}
+ />,
);
- expect(actual).toBe('Foo Bar Baz ');
+ expect(actual).toBe('Foo Bar Baz');
expect(implSpy).not.toHaveBeenCalled();
});
it('supports matchers', () => {
const actual = ReactDOMServer.renderToStaticMarkup(
- ,
+ ,
);
- expect(actual).toBe('Foo B Bar Baz ');
+ expect(actual).toBe('Foo BAR Bar Baz');
expect(implSpy).not.toHaveBeenCalled();
});
});
@@ -356,7 +247,7 @@ describe('Interweave', () => {
Bar'} transform={transform} />,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('replaces the element', () => {
@@ -368,7 +259,7 @@ describe('Interweave', () => {
Bar'} transform={transform} />,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('allows blocked', () => {
@@ -380,7 +271,7 @@ describe('Interweave', () => {
Bar'} transform={transform} />,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('skips transforming tags outside the allowList when transformOnlyAllowList is true', () => {
@@ -421,35 +312,42 @@ describe('Interweave', () => {
,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
});
describe('interleaving', () => {
- const matchers = [new MarkdownBoldMatcher('bold'), new MarkdownItalicMatcher('italic')];
-
it('renders them separately', () => {
const { root } = render(
- ,
+ ,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('renders italic in bold', () => {
const { root } = render(
- ,
+ ,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
it('renders bold in italic', () => {
const { root } = render(
- ,
+ ,
);
- expect(root.findAt(Element, 0)).toMatchSnapshot();
+ expect(root).toMatchSnapshot();
});
});
});
diff --git a/packages/core/tests/Markup.test.tsx b/packages/core/tests/Markup.test.tsx
index 055cb3d3..390ee814 100644
--- a/packages/core/tests/Markup.test.tsx
+++ b/packages/core/tests/Markup.test.tsx
@@ -1,18 +1,11 @@
import React from 'react';
import { render } from 'rut-dom';
-import { Element } from '../src/Element';
import { Markup, MarkupProps } from '../src/Markup';
import { MOCK_MARKUP } from '../src/test';
const options = { log: false, reactElements: false };
describe('Markup', () => {
- it('can change `tagName`', () => {
- const { root } = render( );
-
- expect(root.findOne(Element)).toHaveProp('tagName', 'p');
- });
-
it('allows empty `content` to be passed', () => {
const { root } = render( );
@@ -23,7 +16,7 @@ describe('Markup', () => {
const empty = Foo
;
const { root } = render( );
- expect(root).toContainNode(empty);
+ expect(root.debug(options)).toMatchSnapshot();
});
it('parses the entire document starting from the body', () => {
diff --git a/packages/core/tests/Matcher.test.tsx b/packages/core/tests/Matcher.test.tsx
deleted file mode 100644
index d191f601..00000000
--- a/packages/core/tests/Matcher.test.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-/* eslint-disable react/destructuring-assignment */
-import React from 'react';
-import { Element } from '../src/Element';
-import { CodeTagMatcher, MockMatcher } from '../src/test';
-
-describe('Matcher', () => {
- const matcher = new CodeTagMatcher('foo', '1');
-
- it('errors for html name', () => {
- expect(() => new MockMatcher('html', {})).toThrow('The matcher name "html" is not allowed.');
- });
-
- it('sets names', () => {
- const nameMatcher = new MockMatcher('barBaz', {});
-
- expect(nameMatcher.propName).toBe('barBaz');
- expect(nameMatcher.inverseName).toBe('noBarBaz');
- });
-
- describe('createElement()', () => {
- it('returns a React element from factory', () => {
- expect(matcher.replaceWith('[foo]', { children: 'foo' })).toEqual(
-
- FOO
- ,
- );
- });
-
- it('can use a React component as a custom factory', () => {
- function CustomFactoryComponent(props: { children: React.ReactNode; tagName: string }) {
- return {props.children} ;
- }
-
- const customMatcher = new MockMatcher('foo', {}, CustomFactoryComponent);
-
- expect(customMatcher.createElement('Bar', { tagName: 'div' })).toEqual(
- Bar ,
- );
- });
-
- it('errors if not a React element', () => {
- const customMatcher = new MockMatcher('foo', {});
-
- // @ts-expect-error Allow override
- customMatcher.replaceWith = () => 123;
-
- expect(() => {
- customMatcher.createElement('', {});
- }).toThrow('Invalid React element created from MockMatcher.');
- });
- });
-
- describe('replaceWith()', () => {
- it('returns a React element', () => {
- expect(matcher.replaceWith('[foo]', { children: 'foo' })).toEqual(
-
- FOO
- ,
- );
- });
- });
-
- describe('match()', () => {
- it('does match', () => {
- expect(matcher.match('[foo]')).toEqual({
- index: 0,
- length: 5,
- match: '[foo]',
- children: 'foo',
- customProp: 'foo',
- valid: true,
- void: false,
- });
- });
-
- it('does not match', () => {
- expect(matcher.match('[bar]')).toBeNull();
- });
- });
-
- describe('doMatch()', () => {
- it('returns a match object with index', () => {
- expect(matcher.doMatch('foo', /foo/, () => ({ pass: true }))).toEqual({
- index: 0,
- length: 3,
- match: 'foo',
- pass: true,
- valid: true,
- void: false,
- });
- });
-
- it('returns null if no match', () => {
- expect(matcher.doMatch('bar', /foo/, () => ({ pass: true }))).toBeNull();
- });
- });
-});
diff --git a/packages/core/tests/__snapshots__/Interweave.test.tsx.snap b/packages/core/tests/__snapshots__/Interweave.test.tsx.snap
index c0444197..18316879 100644
--- a/packages/core/tests/__snapshots__/Interweave.test.tsx.snap
+++ b/packages/core/tests/__snapshots__/Interweave.test.tsx.snap
@@ -1,349 +1,291 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Interweave allow list doesnt filter invalid tags and attributes when disabled 1`] = `
-
-
-
-
-
+
+ Outdated font.
+
+ More text with outdated stuff .
+ "
+>
+
+
+
-
-
- Outdated font.
-
-
-
+
+
+ Outdated font.
+
+
+
-
+
-
-
- More text
-
- with outdated stuff
-
- .
-
-
-
+
+
+ More text
+
+ with outdated stuff
+
+ .
+
+
+
-
-
-
-
+
+
+
`;
exports[`Interweave allow list filters invalid tags and attributes 1`] = `
-
-
-
-
-
+
+ Outdated font.
+
+ More text with outdated stuff .
+ "
+>
+
+
+
- Outdated font.
-
+ Outdated font.
+
-
+
-
- More text with outdated stuff.
-
-
+
+ More text with outdated stuff.
+
+
-
-
-
-
+
+
+
`;
-exports[`Interweave allows empty \`content\` to be passed 1`] = `
-
-
-
-`;
+exports[`Interweave allows empty \`content\` to be passed 1`] = ` `;
-exports[`Interweave allows empty \`content\` to be passed when using callbacks 1`] = `
-
-
-
-`;
+exports[`Interweave allows empty \`content\` to be passed when using callbacks 1`] = ` `;
exports[`Interweave block list filters blocked tags and attributes 1`] = `
-
-
-
-
-
+
+ Main content
+
+
+ "
+>
+
+
+
Main content
-
-
-
+
+
+
- Link
-
+ Link
+
-
-
- String
-
-
-
+
+
+ String
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
Sidebar content
-
-
-`;
-
-exports[`Interweave can disable all filters using \`disableFilters\` 1`] = `
-
-
- Foo
-
-
- Bar
-
-
- Baz
-
-
-`;
-
-exports[`Interweave can disable all matchers using \`disableMatchers\` 1`] = `
-
- Foo [b] Bar Baz
-
-`;
-
-exports[`Interweave can pass filters through props 1`] = `
-
-
- Foo
-
-
- Bar
-
-
- Baz
-
-
+
`;
exports[`Interweave can pass matchers through props 1`] = `
-
-
- Foo
-
- B
-
- Bar Baz
-
-
+ ]}>
+ Foo
+
+ BAR
+
+ Bar Baz
+
`;
-exports[`Interweave can pass object based filters through props 1`] = `
-
-
- Foo
-
-
- Bar
-
-
- Baz
-
-
-`;
-
-exports[`Interweave can pass object based matchers through props 1`] = `
-
-
- Foo
-
- B
-
- Bar Baz
-
-
+exports[`Interweave can pass transformers through props 1`] = `
+Bar Baz" transformers={[ , <* /> ]}>
+ Foo
+
+
+ Bar
+
+
+ Baz
+
`;
exports[`Interweave interleaving renders bold in italic 1`] = `
-
-
- This should be
-
- bold and italic
-
- .
-
-
+ , ]}>
+ This should be
+
+ bold and italic
+
+ .
+
`;
exports[`Interweave interleaving renders italic in bold 1`] = `
-
-
- This should be
-
- italic and bold
-
- .
-
-
+ , ]}>
+ This should be
+
+ italic and bold
+
+ .
+
`;
exports[`Interweave interleaving renders them separately 1`] = `
-
-
- This should be
- bold
- and this
- italic
- .
-
-
+ , ]}>
+ This should be
+ bold
+ and this
+ italic
+ .
+
`;
exports[`Interweave line breaks converts line breaks 1`] = `
-
-
- Foo
-
-
-
- Bar
-
-
+
+ Foo
+
+
+
+ Bar
+
`;
-exports[`Interweave line breaks converts line breaks if \`noHtmlExceptMatchers\` is true 1`] = `
-
-
- Foo
-
-
-
- Bar
-
-
+exports[`Interweave line breaks converts line breaks if \`noHtmlExceptInternals\` is true 1`] = `
+
+ Foo
+
+
+
+ Bar
+
`;
exports[`Interweave line breaks doesnt convert line breaks if \`disableLineBreaks\` is true 1`] = `
-
- Foo
-Bar
-
+
+ Foo
+Bar
+
`;
exports[`Interweave line breaks doesnt convert line breaks if \`noHtml\` is true 1`] = `
-
- Foo
-Bar
-
+
+ Foo
+Bar
+
`;
exports[`Interweave line breaks doesnt convert line breaks if it contains HTML 1`] = `
-
-
- Foo
-
-
-
-
- Bar
-
-
+
+ Foo
+
+
+
+
+ Bar
+
`;
exports[`Interweave parseMarkup() can modify the markup using onBeforeParse 1`] = `
-
-
- Foo
-
- Bar
-
- Baz
-
-
+
+ Foo
+
+ Bar
+
+ Baz
+
`;
exports[`Interweave parseMarkup() can modify the tree using onAfterParse 1`] = `
-
-
- Foo
-
- Bar
-
- Baz
-
- Qux
-
-
-
+
+ Foo
+
+ Bar
+
+ Baz
+
+ Qux
+
+
`;
-exports[`Interweave parseMarkup() errors if onAfterParse doesnt return an array 1`] = `"Interweave \`onAfterParse\` must return an array of strings and React elements."`;
-
exports[`Interweave parseMarkup() errors if onBeforeParse doesnt return a string 1`] = `"Interweave \`onBeforeParse\` must return a valid HTML string."`;
exports[`Interweave parsing and rendering handles void elements correctly 1`] = `
-
-
- This has line breaks.
-
-
-
- Horizontal rule.
-
-
-
- An image.
-
-
-
-
-
-`;
-
-exports[`Interweave render() parses HTML 1`] = `
-
-
- Foo
-
- Bar
-
- Baz
-
-
+ " tagName="div">
+ This has line breaks.
+
+
+
+ Horizontal rule.
+
+
+
+ An image.
+
+
+
+
`;
exports[`Interweave renders using a custom container element 1`] = `
-
-
-
- Foo
-
-
- Bar
-
-
- Baz
-
-
-
+
+
+ Foo
+
+
+ Bar
+
+
+ Baz
+
+
`;
exports[`Interweave transform prop allows blocked 1`] = `
diff --git a/packages/core/tests/__snapshots__/Markup.test.tsx.snap b/packages/core/tests/__snapshots__/Markup.test.tsx.snap
index 458e911e..71fb4045 100644
--- a/packages/core/tests/__snapshots__/Markup.test.tsx.snap
+++ b/packages/core/tests/__snapshots__/Markup.test.tsx.snap
@@ -1,58 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Markup allows empty \`content\` to be passed 1`] = `" "`;
+exports[`Markup allows empty \`content\` to be passed 1`] = `""`;
exports[`Markup converts line breaks 1`] = `
-"
- Foo
-
- Bar
- "
+"Foo
+
+Bar"
`;
exports[`Markup doesnt convert line breaks 1`] = `
-"Foo
-Bar "
+"Foo
+Bar"
`;
exports[`Markup doesnt convert line breaks if it contains HTML 1`] = `
-"
- Foo
+"Foo
-
- Bar
- "
+
+Bar"
`;
exports[`Markup parses the entire document starting from the body 1`] = `
-"
-
-
+"
+
Main content
-
-
+
+
+
- "
+"
`;
+
+exports[`Markup will render the \`emptyContent\` if no content exists 1`] = `""`;
From 6f1839d635062f60bf5b993e3d41277398d7c458 Mon Sep 17 00:00:00 2001
From: Miles Johnson
Date: Sat, 12 Mar 2022 14:44:47 -0800
Subject: [PATCH 10/18] Remove attributes prop.
---
packages/core/src/Element.tsx | 10 +-
packages/core/src/Parser.ts | 5 +-
packages/core/tests/Element.test.tsx | 25 +--
packages/core/tests/Parser.test.tsx | 4 +-
.../tests/__snapshots__/HTML.test.tsx.snap | 211 +++++++++++-------
.../tests/__snapshots__/Parser.test.tsx.snap | 30 +--
6 files changed, 139 insertions(+), 146 deletions(-)
diff --git a/packages/core/src/Element.tsx b/packages/core/src/Element.tsx
index 2ce445d1..a7ef0a83 100644
--- a/packages/core/src/Element.tsx
+++ b/packages/core/src/Element.tsx
@@ -1,9 +1,7 @@
-import React, { memo } from 'react';
-import { Attributes } from './types';
+import React from 'react';
export interface ElementProps {
[prop: string]: unknown;
- attributes?: Attributes;
className?: string;
children?: React.ReactNode;
selfClose?: boolean;
@@ -11,18 +9,18 @@ export interface ElementProps {
}
export function Element({
- attributes = {},
className,
children = null,
selfClose = false,
tagName,
+ ...props
}: ElementProps) {
const Tag = tagName as 'span';
return selfClose ? (
-
+
) : (
-
+
{children}
);
diff --git a/packages/core/src/Parser.ts b/packages/core/src/Parser.ts
index ffd7edcb..31c5dc84 100644
--- a/packages/core/src/Parser.ts
+++ b/packages/core/src/Parser.ts
@@ -542,13 +542,10 @@ export class Parser {
// Build the props as it makes it easier to test
const attributes = this.extractAttributes(nextNode);
const elementProps: ElementProps = {
+ ...attributes,
tagName,
};
- if (attributes) {
- elementProps.attributes = attributes;
- }
-
if (config.void) {
elementProps.selfClose = config.void;
}
diff --git a/packages/core/tests/Element.test.tsx b/packages/core/tests/Element.test.tsx
index 85cafc23..90a7847c 100644
--- a/packages/core/tests/Element.test.tsx
+++ b/packages/core/tests/Element.test.tsx
@@ -36,7 +36,7 @@ describe('Element', () => {
it('renders with attributes', () => {
const { root } = render(
-
+
Foo
,
);
@@ -50,14 +50,7 @@ describe('Element', () => {
it('renders with attributes of each type', () => {
const { root } = render(
-
+
Foo
,
);
@@ -84,18 +77,4 @@ describe('Element', () => {
className: 'foo',
});
});
-
- it('can overwrite class name with attributes', () => {
- const { root } = render(
-
- Foo
- ,
- );
-
- expect(root).toHaveRendered();
- expect(root.findOne('div')).toHaveProps({
- children: 'Foo',
- className: 'bar',
- });
- });
});
diff --git a/packages/core/tests/Parser.test.tsx b/packages/core/tests/Parser.test.tsx
index 1a13b689..157f482f 100644
--- a/packages/core/tests/Parser.test.tsx
+++ b/packages/core/tests/Parser.test.tsx
@@ -670,7 +670,7 @@ describe('Parser', () => {
expect(instance.parseNode(element, instance.getTagConfig('span'))).toEqual([
'Foo',
-
+
{['Bar']}
,
'Baz',
@@ -688,7 +688,7 @@ describe('Parser', () => {
}),
).toEqual([
'Foo',
-
+
{['Bar']}
,
'Baz',
diff --git a/packages/core/tests/__snapshots__/HTML.test.tsx.snap b/packages/core/tests/__snapshots__/HTML.test.tsx.snap
index f8655fe6..2494d90e 100644
--- a/packages/core/tests/__snapshots__/HTML.test.tsx.snap
+++ b/packages/core/tests/__snapshots__/HTML.test.tsx.snap
@@ -14,7 +14,7 @@ exports[`html a, ul, li renders 1`] = `
-
+
Website
@@ -25,7 +25,7 @@ exports[`html a, ul, li renders 1`] = `
-
+
Email
@@ -36,7 +36,7 @@ exports[`html a, ul, li renders 1`] = `
-
+
Phone
@@ -53,13 +53,13 @@ exports[`html a, ul, li renders 1`] = `
exports[`html abbr renders 1`] = `
CSS to style your HTML .">
You can use
-
+
CSS
to style your
-
+
HTML
@@ -79,7 +79,7 @@ exports[`html address renders 1`] = `
-
+
jim@rock.com
@@ -89,7 +89,7 @@ exports[`html address renders 1`] = `
-
+
+311-555-2368
@@ -122,7 +122,7 @@ exports[`html article renders 1`] = `
"
>
-
+
@@ -131,7 +131,7 @@ exports[`html article renders 1`] = `
-
+
@@ -149,7 +149,7 @@ exports[`html article renders 1`] = `
-
+
@@ -167,7 +167,7 @@ exports[`html article renders 1`] = `
-
+
@@ -216,7 +216,7 @@ exports[`html audio renders 1`] = `
Your browser does not support the audio
element.
"
>
-
+
Your browser does not support the
@@ -235,11 +235,11 @@ exports[`html audio renders with source 1`] = `
Your browser does not support the audio
element.
"
>
-
+
-
+
@@ -259,13 +259,13 @@ exports[`html b renders 1`] = `
The two most popular science courses offered by the school are
-
+
chemistry
(the study of chemicals and the composition of substances) and
-
+
physics
@@ -292,7 +292,7 @@ exports[`html bdi renders 1`] = `
-
+
Evil Steven
@@ -304,7 +304,7 @@ exports[`html bdi renders 1`] = `
-
+
François fatale
@@ -316,7 +316,7 @@ exports[`html bdi renders 1`] = `
-
+
تیز سمی
@@ -328,7 +328,7 @@ exports[`html bdi renders 1`] = `
-
+
الرجل القوي إيان
@@ -340,7 +340,7 @@ exports[`html bdi renders 1`] = `
-
+
تیز سمی
@@ -358,7 +358,7 @@ exports[`html bdi renders 1`] = `
exports[`html bdo renders 1`] = `
אה, אני אוהב להיות ליד חוף הים">
In the computer's memory, this is stored as
-
+
אה, אני אוהב להיות ליד חוף הים
@@ -374,7 +374,7 @@ exports[`html blockquote renders 1`] = `
– Aldous Huxley, Brave New World "
>
-
+
@@ -409,7 +409,7 @@ exports[`html canvas renders 1`] = `
An alternative text describing what your canvas displays.
"
>
-
+
An alternative text describing what your canvas displays.
@@ -462,14 +462,14 @@ exports[`html caption renders 1`] = `
-
+
He-Man
-
+
Skeletor
@@ -511,7 +511,7 @@ exports[`html cite renders 1`] = `
First sentence in
-
+
Nineteen Eighty-Four
@@ -574,12 +574,12 @@ exports[`html col, colgroup renders 1`] = `
-
+
-
+
@@ -626,7 +626,7 @@ exports[`html dfn renders 1`] = `
A
-
+
validator
@@ -644,11 +644,11 @@ exports[`html div renders 1`] = `
Beware of the leopard
"
>
-
+
-
+
@@ -738,7 +738,7 @@ exports[`html figure, figcaption renders 1`] = `
-
+
@@ -881,7 +881,7 @@ exports[`html header renders 1`] = `
Cute Puppies Express!
"
>
-
+