diff --git a/src/view/com/composer/text-input/web/Tags/__tests__/utils.test.ts b/src/view/com/composer/text-input/web/Tags/__tests__/utils.test.ts new file mode 100644 index 0000000000..fa955f6b78 --- /dev/null +++ b/src/view/com/composer/text-input/web/Tags/__tests__/utils.test.ts @@ -0,0 +1,80 @@ +import {describe, it, expect} from '@jest/globals' + +import {findSuggestionMatch, parsePunctuationFromTag} from '../utils' + +describe('findSuggestionMatch', () => { + it(`finds tag`, () => { + const match = findSuggestionMatch({ + text: 'a #tag', + cursorPosition: 6, + }) + + expect(match).toEqual({ + range: { + from: 2, + to: 6, + }, + query: 'tag', + text: ' #tag', + }) + }) + + it(`validates tag length`, () => { + expect( + findSuggestionMatch({ + text: '#xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + cursorPosition: 65, + }), + ).toEqual({ + range: { + from: 0, + to: 65, + }, + query: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + text: '#xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }) + + expect( + findSuggestionMatch({ + text: '#xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxo', + cursorPosition: 66, + }), + ).toEqual(null) + }) + + it(`reports tag with trailing punctuation`, () => { + const match = findSuggestionMatch({ + text: '#tag!!!', + cursorPosition: 7, + }) + + expect(match).toEqual({ + range: { + from: 0, + to: 7, + }, + query: 'tag!!!', + text: '#tag!!!', + }) + }) +}) + +describe('parsePunctuationFromTag', () => { + it(`parses with punctuation`, () => { + expect(parsePunctuationFromTag('tag!')).toEqual({ + tag: 'tag', + punctuation: '!', + }) + expect(parsePunctuationFromTag('tag!!!')).toEqual({ + tag: 'tag', + punctuation: '!!!', + }) + }) + + it(`parses without punctuation`, () => { + expect(parsePunctuationFromTag('tag')).toEqual({ + tag: 'tag', + punctuation: '', + }) + }) +}) 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 52ed5a95b8..e2e0728dbe 100644 --- a/src/view/com/composer/text-input/web/Tags/plugin.tsx +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -5,6 +5,8 @@ import {Node as ProseMirrorNode} from '@tiptap/pm/model' import {PluginKey} from '@tiptap/pm/state' import Suggestion, {SuggestionOptions} from '@estrattonbailey/tiptap-suggestion' +import {findSuggestionMatch} from './utils' + export type TagOptions = { HTMLAttributes: Record renderLabel: (props: {options: TagOptions; node: ProseMirrorNode}) => string @@ -64,55 +66,13 @@ export const Tags = Node.create({ }, findSuggestionMatch({$position}) { const text = $position.nodeBefore?.isText && $position.nodeBefore.text + const cursorPosition = $position.pos if (!text) { return null } - const regex = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g - const puncRegex = /\p{P}+$/gu - const match = Array.from(text.matchAll(regex)).pop() - - if ( - !match || - match.input === undefined || - match.index === undefined - ) { - return null - } - - const cursorPosition = $position.pos - const startIndex = cursorPosition - text.length - let [matchedString, tag] = match - - const sanitized = tag.replace(puncRegex, '').replace(/^#/, '') - - // 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 - - if (from < cursorPosition && to >= cursorPosition) { - return { - range: { - from, - to, - }, - /** - * 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. - */ - query: tag.replace(/^#/, ''), - // raw text string - text: matchedString, - } - } - - return null + return findSuggestionMatch({text, cursorPosition}) }, }, } diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts new file mode 100644 index 0000000000..ec2a6b2047 --- /dev/null +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -0,0 +1,55 @@ +export function parsePunctuationFromTag(value: string) { + const reg = /(\p{P}+)$/gu + const tag = value.replace(reg, '') + const punctuation = value.match(reg)?.[0] || '' + + return {tag, punctuation} +} + +export function findSuggestionMatch({ + text, + cursorPosition, +}: { + text: string + cursorPosition: number +}) { + const regex = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g + const puncRegex = /\p{P}+$/gu + const match = Array.from(text.matchAll(regex)).pop() + + if (!match || match.input === undefined || match.index === undefined) { + return null + } + + const startIndex = cursorPosition - text.length + let [matchedString, tag] = match + + const sanitized = tag.replace(puncRegex, '').replace(/^#/, '') + + // 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 + + if (from < cursorPosition && to >= cursorPosition) { + return { + range: { + from, + to, + }, + /** + * 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. + */ + query: tag.replace(/^#/, ''), + // raw text string + text: matchedString, + } + } + + return null +} diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx index 3d8f511420..2845c0bdc9 100644 --- a/src/view/com/composer/text-input/web/Tags/view.tsx +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -12,6 +12,8 @@ import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' import {usePalette} from 'lib/hooks/usePalette' import {Text} from 'view/com/util/text/Text' +import {parsePunctuationFromTag} from './utils' + type AutocompleteResult = string type ListProps = SuggestionProps & { autocompleteModel: TagsAutocompleteModel @@ -99,8 +101,7 @@ const Autocomplete = forwardRef( const commit = React.useCallback( (query: string) => { - const tag = query.replace(/(\p{P}+)$/gu, '') - const punctuation = query.match(/(\p{P}+)$/gu)?.[0] || '' + const {tag, punctuation} = parsePunctuationFromTag(query) /* * This values here are passed directly to the `command` method * configured in the `Tags` plugin.