diff --git a/app/client/src/components/designSystems/blueprint/EmailMatcher.tsx b/app/client/src/components/designSystems/blueprint/EmailMatcher.tsx new file mode 100644 index 0000000000..aa5a84bda3 --- /dev/null +++ b/app/client/src/components/designSystems/blueprint/EmailMatcher.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { ChildrenNode, Matcher, MatchResponse, Node } from "interweave"; +import { EmailProps, Email } from "interweave-autolink"; + +type EmailMatch = Pick; + +interface CombinePatternsOptions { + capture?: boolean; + flags?: string; + join?: string; + match?: string; + nonCapture?: boolean; +} + +function combinePatterns( + patterns: RegExp[], + options: CombinePatternsOptions = {}, +) { + let regex = patterns + .map((pattern) => pattern.source) + .join(options.join || ""); + + if (options.capture) { + regex = `(${regex})`; + } else if (options.nonCapture) { + regex = `(?:${regex})`; + } + + if (options.match) { + regex += options.match; + } + + return new RegExp(regex, options.flags || ""); +} + +const URL_HOST = combinePatterns( + [ + /(?:(?:[a-z0-9](?:[-a-z0-9_]*[a-z0-9])?)\.)*/, // Subdomain + /(?:(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)\.)/, // Domain + /(?:[a-z](?:[-a-z0-9]*[a-z0-9])?)/, // TLD + ], + { + capture: true, + }, +); + +const EMAIL_USERNAME_PART = /[.a-z0-9!#$%&?*+=_{|}~-]+/; + +const VALID_ALNUM_CHARS = /[a-z0-9]/; + +const EMAIL_USERNAME = combinePatterns( + [VALID_ALNUM_CHARS, EMAIL_USERNAME_PART, VALID_ALNUM_CHARS], + { + capture: true, + }, +); + +export const EMAIL_PATTERN = combinePatterns([EMAIL_USERNAME, URL_HOST], { + flags: "i", + join: "@", +}); + +export default class EmailMatcher extends Matcher { + replaceWith(children: ChildrenNode, props: EmailProps): Node { + return React.createElement(Email, props, children); + } + + asTag(): string { + return "a"; + } + + match(string: string): MatchResponse | null { + return this.doMatch(string, EMAIL_PATTERN, (matches) => ({ + email: matches[0], + emailParts: { + host: matches[2], + username: matches[1], + }, + })); + } +} diff --git a/app/client/src/components/designSystems/blueprint/TextComponent.tsx b/app/client/src/components/designSystems/blueprint/TextComponent.tsx index f359fec4de..de409d4edf 100644 --- a/app/client/src/components/designSystems/blueprint/TextComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/TextComponent.tsx @@ -4,7 +4,8 @@ import styled from "styled-components"; import { ComponentProps } from "components/designSystems/appsmith/BaseComponent"; import { TextAlign } from "widgets/TextWidget"; import Interweave from "interweave"; -import { UrlMatcher, EmailMatcher } from "interweave-autolink"; +import { UrlMatcher } from "interweave-autolink"; +import EmailMatcher, { EMAIL_PATTERN } from "./EmailMatcher"; import { FontStyleTypes, TextSize, @@ -83,6 +84,10 @@ class TextComponent extends React.Component { textColor, backgroundColor, } = this.props; + let matchers = [new UrlMatcher("url"), new EmailMatcher("email")]; + if (text && EMAIL_PATTERN.test(text)) { + matchers = [new EmailMatcher("email")]; + } return ( { backgroundColor={backgroundColor} className={this.props.isLoading ? "bp3-skeleton" : "bp3-ui-text"} > - + );