Emoji: Account page (#36385)
This commit is contained in:
@@ -5,6 +5,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
import {
|
import {
|
||||||
blockAccount,
|
blockAccount,
|
||||||
@@ -331,9 +332,10 @@ export const Account: React.FC<AccountProps> = ({
|
|||||||
{account &&
|
{account &&
|
||||||
withBio &&
|
withBio &&
|
||||||
(account.note.length > 0 ? (
|
(account.note.length > 0 ? (
|
||||||
<div
|
<EmojiHTML
|
||||||
className='account__note translate'
|
className='account__note translate'
|
||||||
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
htmlString={account.note_emojified}
|
||||||
|
extraEmojis={account.emojis}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className='account__note account__note--missing'>
|
<div className='account__note account__note--missing'>
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { useLinks } from 'mastodon/hooks/useLinks';
|
|||||||
|
|
||||||
import { useAppSelector } from '../store';
|
import { useAppSelector } from '../store';
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
import { isModernEmojiEnabled } from '../utils/environment';
|
||||||
import type { OnElementHandler } from '../utils/html';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { HandledLink } from './status/handled_link';
|
import { useElementHandledLink } from './status/handled_link';
|
||||||
|
|
||||||
interface AccountBioProps {
|
interface AccountBioProps {
|
||||||
className: string;
|
className: string;
|
||||||
@@ -38,23 +37,9 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
[showDropdown, accountId],
|
[showDropdown, accountId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLink = useCallback<OnElementHandler>(
|
const htmlHandlers = useElementHandledLink({
|
||||||
(element, { key, ...props }) => {
|
hashtagAccountId: showDropdown ? accountId : undefined,
|
||||||
if (element instanceof HTMLAnchorElement) {
|
});
|
||||||
return (
|
|
||||||
<HandledLink
|
|
||||||
{...props}
|
|
||||||
key={key as string} // React requires keys to not be part of spread props.
|
|
||||||
href={element.href}
|
|
||||||
text={element.innerText}
|
|
||||||
hashtagAccountId={accountId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
[accountId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const note = useAppSelector((state) => {
|
const note = useAppSelector((state) => {
|
||||||
const account = state.accounts.get(accountId);
|
const account = state.accounts.get(accountId);
|
||||||
@@ -77,9 +62,9 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
htmlString={note}
|
htmlString={note}
|
||||||
extraEmojis={extraEmojis}
|
extraEmojis={extraEmojis}
|
||||||
className={classNames(className, 'translate')}
|
className={classNames(className, 'translate')}
|
||||||
onClickCapture={isModernEmojiEnabled() ? undefined : handleClick}
|
onClickCapture={handleClick}
|
||||||
ref={handleNodeChange}
|
ref={handleNodeChange}
|
||||||
onElement={handleLink}
|
{...htmlHandlers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,42 +1,70 @@
|
|||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
export const AccountFields: React.FC<{
|
import { CustomEmojiProvider } from './emoji/context';
|
||||||
fields: Account['fields'];
|
import { EmojiHTML } from './emoji/html';
|
||||||
limit: number;
|
import { useElementHandledLink } from './status/handled_link';
|
||||||
}> = ({ fields, limit = -1 }) => {
|
|
||||||
const handleClick = useLinks();
|
export const AccountFields: React.FC<Pick<Account, 'fields' | 'emojis'>> = ({
|
||||||
|
fields,
|
||||||
|
emojis,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const htmlHandlers = useElementHandledLink();
|
||||||
|
|
||||||
if (fields.size === 0) {
|
if (fields.size === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-fields' onClickCapture={handleClick}>
|
<CustomEmojiProvider emojis={emojis}>
|
||||||
{fields.take(limit).map((pair, i) => (
|
{fields.map((pair, i) => (
|
||||||
<dl
|
<dl key={i} className={classNames({ verified: pair.verified_at })}>
|
||||||
key={i}
|
<EmojiHTML
|
||||||
className={classNames({ verified: pair.get('verified_at') })}
|
as='dt'
|
||||||
>
|
htmlString={pair.name_emojified}
|
||||||
<dt
|
|
||||||
dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }}
|
|
||||||
className='translate'
|
className='translate'
|
||||||
|
{...htmlHandlers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<dd className='translate' title={pair.get('value_plain') ?? ''}>
|
<dd className='translate' title={pair.value_plain ?? ''}>
|
||||||
{pair.get('verified_at') && (
|
{pair.verified_at && (
|
||||||
<Icon id='check' icon={CheckIcon} className='verified__mark' />
|
<span
|
||||||
)}
|
title={intl.formatMessage(
|
||||||
<span
|
{
|
||||||
dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }}
|
id: 'account.link_verified_on',
|
||||||
|
defaultMessage:
|
||||||
|
'Ownership of this link was checked on {date}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: intl.formatDate(pair.verified_at, dateFormatOptions),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon id='check' icon={CheckIcon} className='verified__mark' />
|
||||||
|
</span>
|
||||||
|
)}{' '}
|
||||||
|
<EmojiHTML
|
||||||
|
as='span'
|
||||||
|
htmlString={pair.value_emojified}
|
||||||
|
{...htmlHandlers}
|
||||||
/>
|
/>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
))}
|
))}
|
||||||
</div>
|
</CustomEmojiProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
|
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
||||||
import type { OnElementHandler } from '@/mastodon/utils/html';
|
import type {
|
||||||
|
OnAttributeHandler,
|
||||||
|
OnElementHandler,
|
||||||
|
} from '@/mastodon/utils/html';
|
||||||
import { htmlStringToComponents } from '@/mastodon/utils/html';
|
import { htmlStringToComponents } from '@/mastodon/utils/html';
|
||||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
|
|
||||||
@@ -16,6 +19,7 @@ interface EmojiHTMLProps {
|
|||||||
extraEmojis?: CustomEmojiMapArg;
|
extraEmojis?: CustomEmojiMapArg;
|
||||||
className?: string;
|
className?: string;
|
||||||
onElement?: OnElementHandler;
|
onElement?: OnElementHandler;
|
||||||
|
onAttribute?: OnAttributeHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
||||||
@@ -26,14 +30,19 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||||||
as: asProp = 'div', // Rename for syntax highlighting
|
as: asProp = 'div', // Rename for syntax highlighting
|
||||||
className = '',
|
className = '',
|
||||||
onElement,
|
onElement,
|
||||||
|
onAttribute,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const contents = useMemo(
|
const contents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
htmlStringToComponents(htmlString, { onText: textToEmojis, onElement }),
|
htmlStringToComponents(htmlString, {
|
||||||
[htmlString, onElement],
|
onText: textToEmojis,
|
||||||
|
onElement,
|
||||||
|
onAttribute,
|
||||||
|
}),
|
||||||
|
[htmlString, onAttribute, onElement],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,6 +69,7 @@ export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||||||
extraEmojis,
|
extraEmojis,
|
||||||
className,
|
className,
|
||||||
onElement,
|
onElement,
|
||||||
|
onAttribute,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const Wrapper = asElement ?? 'div';
|
const Wrapper = asElement ?? 'div';
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { domain } from 'mastodon/initial_state';
|
|||||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { useLinks } from '../hooks/useLinks';
|
||||||
|
|
||||||
export const HoverCardAccount = forwardRef<
|
export const HoverCardAccount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ accountId?: string }
|
{ accountId?: string }
|
||||||
@@ -64,6 +66,8 @@ export const HoverCardAccount = forwardRef<
|
|||||||
!isMutual &&
|
!isMutual &&
|
||||||
!isFollower;
|
!isFollower;
|
||||||
|
|
||||||
|
const handleClick = useLinks();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -105,7 +109,14 @@ export const HoverCardAccount = forwardRef<
|
|||||||
accountId={account.id}
|
accountId={account.id}
|
||||||
className='hover-card__bio'
|
className='hover-card__bio'
|
||||||
/>
|
/>
|
||||||
<AccountFields fields={account.fields} limit={2} />
|
|
||||||
|
<div className='account-fields' onClickCapture={handleClick}>
|
||||||
|
<AccountFields
|
||||||
|
fields={account.fields.take(2)}
|
||||||
|
emojis={account.emojis}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{note && note.length > 0 && (
|
{note && note.length > 0 && (
|
||||||
<dl className='hover-card__note'>
|
<dl className='hover-card__note'>
|
||||||
<dt className='hover-card__note-label'>
|
<dt className='hover-card__note-label'>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
import type { ComponentProps, FC } from 'react';
|
import type { ComponentProps, FC } from 'react';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { OnElementHandler } from '@/mastodon/utils/html';
|
||||||
|
|
||||||
export interface HandledLinkProps {
|
export interface HandledLinkProps {
|
||||||
href: string;
|
href: string;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -77,3 +80,31 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useElementHandledLink = ({
|
||||||
|
hashtagAccountId,
|
||||||
|
mentionAccountId,
|
||||||
|
}: {
|
||||||
|
hashtagAccountId?: string;
|
||||||
|
mentionAccountId?: string;
|
||||||
|
} = {}) => {
|
||||||
|
const onElement = useCallback<OnElementHandler>(
|
||||||
|
(element, { key, ...props }) => {
|
||||||
|
if (element instanceof HTMLAnchorElement) {
|
||||||
|
return (
|
||||||
|
<HandledLink
|
||||||
|
{...props}
|
||||||
|
key={key as string} // React requires keys to not be part of spread props.
|
||||||
|
href={element.href}
|
||||||
|
text={element.innerText}
|
||||||
|
hashtagAccountId={hashtagAccountId}
|
||||||
|
mentionAccountId={mentionAccountId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
[hashtagAccountId, mentionAccountId],
|
||||||
|
);
|
||||||
|
return { onElement };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
|
||||||
|
import { isModernEmojiEnabled } from '../utils/environment';
|
||||||
|
import type { OnAttributeHandler } from '../utils/html';
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
const stripRelMe = (html: string) => {
|
const stripRelMe = (html: string) => {
|
||||||
|
if (isModernEmojiEnabled()) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||||
|
|
||||||
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
|
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
|
||||||
@@ -15,7 +22,23 @@ const stripRelMe = (html: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const body = document.querySelector('body');
|
const body = document.querySelector('body');
|
||||||
return body ? { __html: body.innerHTML } : undefined;
|
return body?.innerHTML ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
||||||
|
if (name === 'rel' && tagName === 'a') {
|
||||||
|
if (value === 'me') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
.split(' ')
|
||||||
|
.filter((x) => x !== 'me')
|
||||||
|
.join(' '),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,6 +47,10 @@ interface Props {
|
|||||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||||
<span className='verified-badge'>
|
<span className='verified-badge'>
|
||||||
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
||||||
<span dangerouslySetInnerHTML={stripRelMe(link)} />
|
<EmojiHTML
|
||||||
|
as='span'
|
||||||
|
htmlString={stripRelMe(link)}
|
||||||
|
onAttribute={onAttribute}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { Helmet } from 'react-helmet';
|
|||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { AccountBio } from '@/mastodon/components/account_bio';
|
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||||
|
import { AccountFields } from '@/mastodon/components/account_fields';
|
||||||
import { DisplayName } from '@/mastodon/components/display_name';
|
import { DisplayName } from '@/mastodon/components/display_name';
|
||||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
|
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
|
||||||
@@ -186,14 +186,6 @@ const titleFromAccount = (account: Account) => {
|
|||||||
return `${prefix} (@${acct})`;
|
return `${prefix} (@${acct})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AccountHeader: React.FC<{
|
export const AccountHeader: React.FC<{
|
||||||
accountId: string;
|
accountId: string;
|
||||||
hideTabs?: boolean;
|
hideTabs?: boolean;
|
||||||
@@ -891,46 +883,7 @@ export const AccountHeader: React.FC<{
|
|||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{fields.map((pair, i) => (
|
<AccountFields fields={fields} emojis={account.emojis} />
|
||||||
<dl
|
|
||||||
key={i}
|
|
||||||
className={classNames({
|
|
||||||
verified: pair.verified_at,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<dt
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: pair.name_emojified,
|
|
||||||
}}
|
|
||||||
title={pair.name}
|
|
||||||
className='translate'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<dd className='translate' title={pair.value_plain ?? ''}>
|
|
||||||
{pair.verified_at && (
|
|
||||||
<span
|
|
||||||
title={intl.formatMessage(messages.linkVerifiedOn, {
|
|
||||||
date: intl.formatDate(
|
|
||||||
pair.verified_at,
|
|
||||||
dateFormatOptions,
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
id='check'
|
|
||||||
icon={CheckIcon}
|
|
||||||
className='verified__mark'
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}{' '}
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: pair.value_emojified,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { isFulfilled, isRejected } from '@reduxjs/toolkit';
|
|||||||
import { openURL } from 'mastodon/actions/search';
|
import { openURL } from 'mastodon/actions/search';
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { isModernEmojiEnabled } from '../utils/environment';
|
||||||
|
|
||||||
const isMentionClick = (element: HTMLAnchorElement) =>
|
const isMentionClick = (element: HTMLAnchorElement) =>
|
||||||
element.classList.contains('mention') &&
|
element.classList.contains('mention') &&
|
||||||
!element.classList.contains('hashtag');
|
!element.classList.contains('hashtag');
|
||||||
@@ -53,6 +55,11 @@ export const useLinks = (skipHashtags?: boolean) => {
|
|||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
// Exit early if modern emoji is enabled, as this is handled by HandledLink.
|
||||||
|
if (isModernEmojiEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const target = (e.target as HTMLElement).closest('a');
|
const target = (e.target as HTMLElement).closest('a');
|
||||||
|
|
||||||
if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
|
if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
|
||||||
|
|||||||
@@ -41,18 +41,22 @@ export type OnElementHandler<
|
|||||||
extra: Arg,
|
extra: Arg,
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
|
|
||||||
|
export type OnAttributeHandler<
|
||||||
|
Arg extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
> = (
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
tagName: string,
|
||||||
|
extra: Arg,
|
||||||
|
) => [string, unknown] | undefined | null;
|
||||||
|
|
||||||
export interface HTMLToStringOptions<
|
export interface HTMLToStringOptions<
|
||||||
Arg extends Record<string, unknown> = Record<string, unknown>,
|
Arg extends Record<string, unknown> = Record<string, unknown>,
|
||||||
> {
|
> {
|
||||||
maxDepth?: number;
|
maxDepth?: number;
|
||||||
onText?: (text: string, extra: Arg) => React.ReactNode;
|
onText?: (text: string, extra: Arg) => React.ReactNode;
|
||||||
onElement?: OnElementHandler<Arg>;
|
onElement?: OnElementHandler<Arg>;
|
||||||
onAttribute?: (
|
onAttribute?: OnAttributeHandler<Arg>;
|
||||||
name: string,
|
|
||||||
value: string,
|
|
||||||
tagName: string,
|
|
||||||
extra: Arg,
|
|
||||||
) => [string, unknown] | null;
|
|
||||||
allowedTags?: AllowedTagsType;
|
allowedTags?: AllowedTagsType;
|
||||||
extraArgs?: Arg;
|
extraArgs?: Arg;
|
||||||
}
|
}
|
||||||
@@ -140,44 +144,44 @@ export function htmlStringToComponents<Arg extends Record<string, unknown>>(
|
|||||||
|
|
||||||
// Custom attribute handler.
|
// Custom attribute handler.
|
||||||
if (onAttribute) {
|
if (onAttribute) {
|
||||||
const result = onAttribute(
|
const result = onAttribute(name, attr.value, tagName, extraArgs);
|
||||||
name,
|
// Rewrite this attribute.
|
||||||
attr.value,
|
|
||||||
node.tagName.toLowerCase(),
|
|
||||||
extraArgs,
|
|
||||||
);
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const [cbName, value] = result;
|
const [cbName, value] = result;
|
||||||
props[cbName] = value;
|
props[cbName] = value;
|
||||||
}
|
continue;
|
||||||
} else {
|
} else if (result === null) {
|
||||||
// Check global attributes first, then tag-specific ones.
|
// Explicitly remove this attribute.
|
||||||
const globalAttr = globalAttributes[name];
|
|
||||||
const tagAttr = tagInfo.attributes?.[name];
|
|
||||||
|
|
||||||
// Exit if neither global nor tag-specific attribute is allowed.
|
|
||||||
if (!globalAttr && !tagAttr) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename if needed.
|
|
||||||
if (typeof tagAttr === 'string') {
|
|
||||||
name = tagAttr;
|
|
||||||
} else if (typeof globalAttr === 'string') {
|
|
||||||
name = globalAttr;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value: string | boolean | number = attr.value;
|
|
||||||
|
|
||||||
// Handle boolean attributes.
|
|
||||||
if (value === 'true') {
|
|
||||||
value = true;
|
|
||||||
} else if (value === 'false') {
|
|
||||||
value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
props[name] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check global attributes first, then tag-specific ones.
|
||||||
|
const globalAttr = globalAttributes[name];
|
||||||
|
const tagAttr = tagInfo.attributes?.[name];
|
||||||
|
|
||||||
|
// Exit if neither global nor tag-specific attribute is allowed.
|
||||||
|
if (!globalAttr && !tagAttr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename if needed.
|
||||||
|
if (typeof tagAttr === 'string') {
|
||||||
|
name = tagAttr;
|
||||||
|
} else if (typeof globalAttr === 'string') {
|
||||||
|
name = globalAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: string | boolean | number = attr.value;
|
||||||
|
|
||||||
|
// Handle boolean attributes.
|
||||||
|
if (value === 'true') {
|
||||||
|
value = true;
|
||||||
|
} else if (value === 'false') {
|
||||||
|
value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
props[name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If onElement is provided, use it to create the element.
|
// If onElement is provided, use it to create the element.
|
||||||
|
|||||||
Reference in New Issue
Block a user