Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next-gen, take 2 #227

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
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