add a test
This commit is contained in:
@@ -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: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,6 +5,8 @@ import {Node as ProseMirrorNode} from '@tiptap/pm/model'
|
|||||||
import {PluginKey} from '@tiptap/pm/state'
|
import {PluginKey} from '@tiptap/pm/state'
|
||||||
import Suggestion, {SuggestionOptions} from '@estrattonbailey/tiptap-suggestion'
|
import Suggestion, {SuggestionOptions} from '@estrattonbailey/tiptap-suggestion'
|
||||||
|
|
||||||
|
import {findSuggestionMatch} from './utils'
|
||||||
|
|
||||||
export type TagOptions = {
|
export type TagOptions = {
|
||||||
HTMLAttributes: Record<string, any>
|
HTMLAttributes: Record<string, any>
|
||||||
renderLabel: (props: {options: TagOptions; node: ProseMirrorNode}) => string
|
renderLabel: (props: {options: TagOptions; node: ProseMirrorNode}) => string
|
||||||
@@ -64,55 +66,13 @@ export const Tags = Node.create<TagOptions>({
|
|||||||
},
|
},
|
||||||
findSuggestionMatch({$position}) {
|
findSuggestionMatch({$position}) {
|
||||||
const text = $position.nodeBefore?.isText && $position.nodeBefore.text
|
const text = $position.nodeBefore?.isText && $position.nodeBefore.text
|
||||||
|
const cursorPosition = $position.pos
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g
|
return findSuggestionMatch({text, cursorPosition})
|
||||||
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
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
|
|||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
|
||||||
|
import {parsePunctuationFromTag} from './utils'
|
||||||
|
|
||||||
type AutocompleteResult = string
|
type AutocompleteResult = string
|
||||||
type ListProps = SuggestionProps<AutocompleteResult> & {
|
type ListProps = SuggestionProps<AutocompleteResult> & {
|
||||||
autocompleteModel: TagsAutocompleteModel
|
autocompleteModel: TagsAutocompleteModel
|
||||||
@@ -99,8 +101,7 @@ const Autocomplete = forwardRef<AutocompleteRef, ListProps>(
|
|||||||
|
|
||||||
const commit = React.useCallback(
|
const commit = React.useCallback(
|
||||||
(query: string) => {
|
(query: string) => {
|
||||||
const tag = query.replace(/(\p{P}+)$/gu, '')
|
const {tag, punctuation} = parsePunctuationFromTag(query)
|
||||||
const punctuation = query.match(/(\p{P}+)$/gu)?.[0] || ''
|
|
||||||
/*
|
/*
|
||||||
* This values here are passed directly to the `command` method
|
* This values here are passed directly to the `command` method
|
||||||
* configured in the `Tags` plugin.
|
* configured in the `Tags` plugin.
|
||||||
|
|||||||
Reference in New Issue
Block a user