From ee3d1ff40d62956226d34f719d4af2efaf64d02a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 23 Oct 2023 15:04:46 -0500 Subject: [PATCH] Use new more restrictive regex --- src/lib/strings/hashtags.ts | 7 ++++-- .../com/composer/text-input/TextInput.tsx | 2 ++ .../text-input/mobile/TagsAutocomplete.tsx | 21 +++++++++------- .../composer/text-input/web/Tags/plugin.tsx | 10 +++++--- .../com/composer/text-input/web/Tags/utils.ts | 25 ++++++++++++------- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/lib/strings/hashtags.ts b/src/lib/strings/hashtags.ts index 3e057f6927..1969718b88 100644 --- a/src/lib/strings/hashtags.ts +++ b/src/lib/strings/hashtags.ts @@ -1,6 +1,9 @@ -export const TAG_REGEX = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/gi +export const TAG_REGEX = + /(?:^|\s)(#[\p{L}\p{Emoji_Presentation}\p{Emoji_Modifier_Base}\p{Extended_Pictographic}]{1}[\p{L}\p{Emoji_Presentation}\p{Emoji_Modifier_Base}\p{Extended_Pictographic}\d_-]*)/giu +export const LOOSE_TAG_REGEX = + /(?:^|\s)(#[\p{L}\p{Emoji_Presentation}\p{Emoji_Modifier_Base}\p{Extended_Pictographic}]{1}[\p{L}\p{Emoji_Presentation}\p{Emoji_Modifier_Base}\p{Extended_Pictographic}\d_-]*\S*)/giu export const ENDING_PUNCTUATION_REGEX = /\p{P}+$/gu -export const LEADING_HASH_REGEX = /^#/ +export const LEADING_HASH_REGEX = /^#/g export function sanitize(tagString: string) { return tagString diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index f853c6e0df..09582aa4ea 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -236,6 +236,8 @@ export const TextInput = forwardRef(function TextInputImpl( }) }, [richtext, pal.link, pal.text]) + console.log('render') + return ( 66) continue + if (tag.length > 66 || index === undefined) continue - const from = match.index + matchedString.indexOf(tag) + const from = index + matchedString.indexOf(tag) const to = from + tag.length if (position >= from && position <= to) { @@ -26,11 +29,11 @@ export function getHashtagAt(text: string, position: number) { * show autocomplete after a single # is typed * AND the cursor is next to the # */ - const hashRegex = LEADING_HASH_REGEX - let hashMatch - while ((hashMatch = hashRegex.exec(text))) { - if (position >= hashMatch.index && position <= hashMatch.index + 1) { - return {value: '', index: hashMatch.index} + for (const match of Array.from(text.matchAll(LEADING_HASH_REGEX))) { + const {index} = match + if (index === undefined) continue + if (position >= index && position <= index + 1) { + return {value: '', index} } } diff --git a/src/view/com/composer/text-input/web/Tags/plugin.tsx b/src/view/com/composer/text-input/web/Tags/plugin.tsx index 5db789b4cc..5724b7e409 100644 --- a/src/view/com/composer/text-input/web/Tags/plugin.tsx +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -25,8 +25,8 @@ export const Tags = Node.create({ addOptions() { return { HTMLAttributes: {}, - renderLabel({options, node}) { - return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` + renderLabel({node}) { + return `#${node.attrs.id}` }, suggestion: { char: '#', @@ -61,11 +61,15 @@ export const Tags = Node.create({ window.getSelection()?.collapseToEnd() }, + /** + * This method and `findSuggestionMatch` below both have to return a + * truthy value, otherwise the suggestiond plugin will call `onExit` + * and we lose the ability to add a tag + */ allow: ({state, range}) => { const $from = state.doc.resolve(range.from) const type = state.schema.nodes[this.name] const allow = !!$from.parent.type.contentMatch.matchType(type) - return allow }, findSuggestionMatch({$position}) { diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts index 81e6ee4b6b..3e46957170 100644 --- a/src/view/com/composer/text-input/web/Tags/utils.ts +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -1,5 +1,5 @@ import { - TAG_REGEX, + LOOSE_TAG_REGEX, ENDING_PUNCTUATION_REGEX, LEADING_HASH_REGEX, } from 'lib/strings/hashtags' @@ -12,6 +12,13 @@ export function parsePunctuationFromTag(value: string) { return {tag, punctuation} } +/** + * A result must be returned from this method in order for the suggestion + * plugin to remain active and allow for the user to select a suggestion. + * + * That's why we use the loose regex form that includes trialing punctuation. + * We strip that our later. + */ export function findSuggestionMatch({ text, cursorPosition, @@ -19,24 +26,25 @@ export function findSuggestionMatch({ text: string cursorPosition: number }) { - const match = Array.from(text.matchAll(TAG_REGEX)).pop() + const match = Array.from(text.matchAll(LOOSE_TAG_REGEX)).pop() if (!match || match.input === undefined || match.index === undefined) { return null } const startIndex = cursorPosition - text.length - let [matchedString, tag] = match + let [matchedString, looselyMatchedTag] = match - const sanitized = tag + const sanitized = looselyMatchedTag .replace(ENDING_PUNCTUATION_REGEX, '') .replace(LEADING_HASH_REGEX, '') // one of our hashtag spec rules if (sanitized.length > 64) return null - const from = startIndex + match.index + matchedString.indexOf(tag) - const to = from + tag.length + const from = + startIndex + match.index + matchedString.indexOf(looselyMatchedTag) + const to = from + looselyMatchedTag.length if (from < cursorPosition && to >= cursorPosition) { return { @@ -48,10 +56,9 @@ export function findSuggestionMatch({ * This is passed to the `items({ query })` method configured in * `createTagsAutocomplete`. * - * We parse out the punctuation later, but we don't want to pass - * the # to the search query. + * We parse out the punctuation later. */ - query: tag.replace(LEADING_HASH_REGEX, ''), + query: looselyMatchedTag.replace(LEADING_HASH_REGEX, ''), // raw text string text: matchedString, }