Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .config/beemo/eslint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
ignore: ['*.d.ts'],
rules: {
'jest/no-conditional-in-test': 'off',
},
Expand Down
4 changes: 3 additions & 1 deletion .config/beemo/jest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export default {
setupFilesAfterEnv: ['jest-rut'],
testEnvironment: 'jsdom',
timers: 'legacy',
fakeTimers: {
legacyFakeTimers: true,
},
};
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,24 @@
"devDependencies": {
"@beemo/cli": "^2.0.6",
"@beemo/core": "^2.1.4",
"@beemo/dev": "^1.7.8",
"@types/lodash": "^4.14.179",
"@beemo/dev": "^1.7.13",
"@types/lodash": "^4.14.182",
"@types/parse5": "^6.0.3",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"@types/react": "^17.0.44",
"@types/react-dom": "^17.0.16",
"@types/react-window": "^1.8.5",
"babel-loader": "^8.2.3",
"conventional-changelog-beemo": "^3.0.0",
"babel-loader": "^8.2.5",
"conventional-changelog-beemo": "^3.0.1",
"emojibase": "^6.1.0",
"emojibase-test-utils": "^7.0.0",
"eslint-plugin-rut": "^2.0.0",
"jest-rut": "^2.0.0",
"packemon": "^1.14.0",
"packemon": "^1.15.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"rut-dom": "^2.0.0",
"serve": "^13.0.2",
"webpack": "^5.70.0",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"
},
"dependencies": {
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/Element.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import React from 'react';
import { ElementProps } from './types';

export interface ElementProps {
[prop: string]: unknown;
className?: string;
children?: React.ReactNode;
selfClose?: boolean;
tagName: string;
}

export function Element({
attributes = {},
className,
children = null,
selfClose = false,
tagName,
...props
}: ElementProps) {
const Tag = tagName as 'span';

return selfClose ? (
<Tag className={className} {...attributes} />
<Tag className={className} {...props} />
) : (
<Tag className={className} {...attributes}>
<Tag className={className} {...props}>
{children}
</Tag>
);
Expand Down
21 changes: 0 additions & 21 deletions packages/core/src/Filter.ts

This file was deleted.

129 changes: 66 additions & 63 deletions packages/core/src/Interweave.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,81 @@
/* eslint-disable promise/prefer-await-to-callbacks */
import React from 'react';
import { Markup } from './Markup';
import { Parser } from './Parser';
import { InterweaveProps } from './types';
import React, { useMemo } from 'react';
import { MarkupProps } from './Markup';
import { MatcherInterface, Parser, TransformerInterface } from './Parser';
import { CommonInternals, OnAfterParse, OnBeforeParse } from './types';

export interface InterweaveProps extends MarkupProps {
/** List of transformers to apply to elements. */
transformers?: TransformerInterface[];
/** List of matchers to apply to the content. */
matchers?: MatcherInterface[];
/** 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 {
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));
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 (matcher.onAfterParse) {
afterCallbacks.push(matcher.onAfterParse.bind(matcher));
if (matchers) {
inheritCallbacks(matchers);
}
});

// Trigger before callbacks
const markup = beforeCallbacks.reduce((string, callback) => {
const nextString = callback(string, props);
if (transformers) {
inheritCallbacks(transformers);
}

if (__DEV__ && typeof nextString !== 'string') {
throw new TypeError('Interweave `onBeforeParse` must return a valid HTML string.');
if (onBeforeParse) {
beforeCallbacks.push(onBeforeParse);
}

return nextString;
}, content ?? '');
if (onAfterParse) {
afterCallbacks.push(onAfterParse);
}

// Trigger before callbacks
const markup = beforeCallbacks.reduce((string, before) => {
const nextString = before(string, 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), nodes);
}

return nextNodes;
}, parser.parse());

return (
<Markup
attributes={attributes}
className={className}
// eslint-disable-next-line react/destructuring-assignment
containerTagName={props.containerTagName}
emptyContent={emptyContent}
noWrap={noWrap}
parsedContent={nodes.length === 0 ? undefined : nodes}
tagName={tagName}
/>
);
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}</>;
}
58 changes: 17 additions & 41 deletions packages/core/src/Markup.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,23 @@
/* eslint-disable react/jsx-fragments */
import React, { useMemo } from 'react';
import { Parser, ParserProps } from './Parser';

import React from 'react';
import { Element } from './Element';
import { Parser } from './Parser';
import { MarkupProps } from './types';
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 {
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;
}
}
const { content, emptyContent } = props;

if (!mainContent) {
mainContent = emptyContent;
}

if (noWrap) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <React.Fragment>{mainContent}</React.Fragment>;
}

return (
<Element attributes={attributes} className={className} tagName={tag}>
{mainContent}
</Element>
const mainContent = useMemo(
() => 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
[content],
);

// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{mainContent ?? emptyContent}</>;
}
88 changes: 0 additions & 88 deletions packages/core/src/Matcher.ts

This file was deleted.

Loading