snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# @tiptap/extension-mention
[![Version](https://img.shields.io/npm/v/@tiptap/extension-mention.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-mention)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-mention.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-mention.svg)](https://www.npmjs.com/package/@tiptap/extension-mention)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## Introduction
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
## Official Documentation
Documentation can be found on the [Tiptap website](https://tiptap.dev).
## License
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).

View File

@@ -0,0 +1,54 @@
{
"name": "@tiptap/extension-mention",
"version": "3.30.3",
"description": "mention extension for tiptap",
"keywords": [
"tiptap",
"tiptap extension"
],
"homepage": "https://tiptap.dev",
"bugs": {
"url": "https://github.com/ueberdosis/tiptap/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ueberdosis/tiptap",
"directory": "packages/extension-mention"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"files": [
"src",
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": {
"import": "./dist/index.d.ts",
"require": "./dist/index.d.cts"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "vp pack"
},
"devDependencies": {
"@tiptap/core": "workspace:^",
"@tiptap/pm": "workspace:^",
"@tiptap/suggestion": "workspace:^"
},
"peerDependencies": {
"@tiptap/core": "workspace:*",
"@tiptap/pm": "workspace:*",
"@tiptap/suggestion": "workspace:*"
}
}

View File

@@ -0,0 +1,5 @@
import { Mention } from './mention.js'
export * from './mention.js'
export default Mention

View File

@@ -0,0 +1,48 @@
import { Mention } from './mention.js'
import { describe, expect, it } from 'vite-plus/test'
describe('mention Markdown', () => {
it('parses mapped attributes', () => {
const tokenizer = Mention.config.markdownTokenizer
expect(tokenizer).toBeDefined()
const token = tokenizer?.tokenize('[@ id="feature" char="#"]', [], null as any)
expect(token).toMatchObject({
attributes: {
id: 'feature',
mentionSuggestionChar: '#',
},
})
})
it('parses malformed attributes in linear time', () => {
const tokenizer = Mention.config.markdownTokenizer
expect(tokenizer).toBeDefined()
const measure = (attributes: string) => {
const startedAt = performance.now()
tokenizer?.tokenize(`[@ ${attributes}]`, [], null as any)
return performance.now() - startedAt
}
measure('warmup="true"')
const sizes = [12_288, 24_576]
sizes.forEach(size => {
const attack = '0'.repeat(size)
const control = '-'.repeat(attack.length)
const controlDuration = measure(control)
const attackDuration = measure(attack)
expect(attackDuration).toBeLessThan(controlDuration * 20 + 100)
})
})
it('only parses attributes at attribute boundaries', () => {
const tokenizer = Mention.config.markdownTokenizer
const token = tokenizer?.tokenize('[@ id="feature" prefix:char="#"]', [], null as any)
expect(token).toMatchObject({ attributes: { id: 'feature' } })
})
})

View File

@@ -0,0 +1,367 @@
import type { Editor } from '@tiptap/core'
import { createInlineMarkdownSpec, mergeAttributes, Node } from '@tiptap/core'
import type { DOMOutputSpec } from '@tiptap/pm/model'
import { Node as PMNode } from '@tiptap/pm/model'
import type { SuggestionOptions } from '@tiptap/suggestion'
import { Suggestion } from '@tiptap/suggestion'
import { getSuggestionOptions } from './utils/get-default-suggestion-attributes.js'
// See `addAttributes` below
export interface MentionNodeAttrs {
/**
* The identifier for the selected item that was mentioned, stored as a `data-id`
* attribute.
*/
id: string | null
/**
* The label to be rendered by the editor as the displayed text for this mentioned
* item, if provided. Stored as a `data-label` attribute. See `renderLabel`.
*/
label?: string | null
/**
* The character that triggers the suggestion, stored as
* `data-mention-suggestion-char` attribute.
*/
mentionSuggestionChar?: string
}
export interface MentionOptions<
SuggestionItem = any,
Attrs extends Record<string, any> = MentionNodeAttrs,
> {
/**
* The HTML attributes for a mention node.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>
/**
* A function to render the label of a mention.
* @deprecated use renderText and renderHTML instead
* @param props The render props
* @returns The label
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/
renderLabel?: (props: {
options: MentionOptions<SuggestionItem, Attrs>
node: PMNode
suggestion: SuggestionOptions | null
}) => string
/**
* A function to render the text of a mention.
* @param props The render props
* @returns The text
* @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
*/
renderText: (props: {
options: MentionOptions<SuggestionItem, Attrs>
node: PMNode
suggestion: SuggestionOptions | null
}) => string
/**
* A function to render the HTML of a mention.
* @param props The render props
* @returns The HTML as a ProseMirror DOM Output Spec
* @example ({ options, node }) => ['span', { 'data-type': 'mention' }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`]
*/
renderHTML: (props: {
options: MentionOptions<SuggestionItem, Attrs>
node: PMNode
suggestion: SuggestionOptions | null
}) => DOMOutputSpec
/**
* Whether to delete the trigger character with backspace.
* @default false
*/
deleteTriggerWithBackspace: boolean
/**
* The suggestion options, when you want to support multiple triggers.
*
* With this parameter, you can define multiple types of mention. For example, you can use the `@` character
* to mention users and the `#` character to mention tags.
*
* @default [{ char: '@', pluginKey: MentionPluginKey }]
* @example [{ char: '@', pluginKey: MentionPluginKey }, { char: '#', pluginKey: new PluginKey('hashtag') }]
*/
suggestions: Array<Omit<SuggestionOptions<SuggestionItem, Attrs>, 'editor'>>
/**
* The suggestion options, when you want to support only one trigger. To support multiple triggers, use the
* `suggestions` parameter instead.
*
* @default {}
* @example { char: '@', pluginKey: MentionPluginKey, command: ({ editor, range, props }) => { ... } }
*/
suggestion: Omit<SuggestionOptions<SuggestionItem, Attrs>, 'editor'>
}
interface GetSuggestionsOptions {
editor?: Editor
options: MentionOptions
name: string
}
/**
* Returns the suggestions for the mention extension.
*
* @param options The extension options
* @returns the suggestions
*/
function getSuggestions(options: GetSuggestionsOptions) {
return (
options.options.suggestions.length ? options.options.suggestions : [options.options.suggestion]
).map(suggestion =>
getSuggestionOptions({
// @ts-ignore `editor` can be `undefined` when converting the document to HTML with the HTML utility
editor: options.editor,
overrideSuggestionOptions: suggestion,
extensionName: options.name,
char: suggestion.char,
}),
)
}
/**
* Returns the suggestion options of the mention that has a given character trigger. If not
* found, it returns the first suggestion.
*
* @param options The extension options
* @param char The character that triggers the mention
* @returns The suggestion options
*/
function getSuggestionFromChar(options: GetSuggestionsOptions, char: string) {
const suggestions = getSuggestions(options)
const suggestion = suggestions.find(s => s.char === char)
if (suggestion) {
return suggestion
}
if (suggestions.length) {
return suggestions[0]
}
return null
}
/**
* This extension allows you to insert mentions into the editor.
* @see https://www.tiptap.dev/api/extensions/mention
*/
export const Mention = Node.create<MentionOptions>({
name: 'mention',
priority: 101,
addOptions() {
return {
HTMLAttributes: {},
renderText({ node, suggestion }) {
return `${suggestion?.char ?? '@'}${node.attrs.label ?? node.attrs.id}`
},
deleteTriggerWithBackspace: false,
renderHTML({ options, node, suggestion }) {
return [
'span',
mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),
`${suggestion?.char ?? '@'}${node.attrs.label ?? node.attrs.id}`,
]
},
suggestions: [],
suggestion: {},
}
},
group: 'inline',
inline: true,
selectable: false,
atom: true,
addAttributes() {
return {
id: {
default: null,
parseHTML: element => element.getAttribute('data-id'),
renderHTML: attributes => {
if (!attributes.id) {
return {}
}
return {
'data-id': attributes.id,
}
},
},
label: {
default: null,
parseHTML: element => element.getAttribute('data-label'),
renderHTML: attributes => {
if (!attributes.label) {
return {}
}
return {
'data-label': attributes.label,
}
},
},
// When there are multiple types of mentions, this attribute helps distinguish them
mentionSuggestionChar: {
default: '@',
parseHTML: element => element.getAttribute('data-mention-suggestion-char'),
renderHTML: attributes => {
return {
'data-mention-suggestion-char': attributes.mentionSuggestionChar,
}
},
},
}
},
parseHTML() {
return [
{
tag: `span[data-type="${this.name}"]`,
},
]
},
renderHTML({ node, HTMLAttributes }) {
const suggestion = getSuggestionFromChar(this, node.attrs.mentionSuggestionChar)
if (this.options.renderLabel !== undefined) {
console.warn('renderLabel is deprecated use renderText and renderHTML instead')
return [
'span',
mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes),
this.options.renderLabel({
options: this.options,
node,
suggestion,
}),
]
}
const mergedOptions = { ...this.options }
mergedOptions.HTMLAttributes = mergeAttributes(
{ 'data-type': this.name },
this.options.HTMLAttributes,
HTMLAttributes,
)
const html = this.options.renderHTML({
options: mergedOptions,
node,
suggestion,
})
if (typeof html === 'string') {
return [
'span',
mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes),
html,
]
}
return html
},
...createInlineMarkdownSpec({
nodeName: 'mention',
name: '@',
selfClosing: true,
allowedAttributes: ['id', 'label', { name: 'mentionSuggestionChar', skipIfDefault: '@' }],
parseAttributes: (attrString: string) => {
const attrs: Record<string, any> = {}
const regex = /(?:^|\s)(\w+)=(?:"([^"]*)"|'([^']*)')/g
let match = regex.exec(attrString)
while (match !== null) {
const [, key, doubleQuoted, singleQuoted] = match
const value = doubleQuoted ?? singleQuoted
attrs[key === 'char' ? 'mentionSuggestionChar' : key] = value
match = regex.exec(attrString)
}
return attrs
},
serializeAttributes: (attrs: Record<string, any>) => {
return Object.entries(attrs)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
const serializedKey = key === 'mentionSuggestionChar' ? 'char' : key
return `${serializedKey}="${value}"`
})
.join(' ')
},
}),
renderText({ node }) {
const args = {
options: this.options,
node,
suggestion: getSuggestionFromChar(this, node.attrs.mentionSuggestionChar),
}
if (this.options.renderLabel !== undefined) {
console.warn('renderLabel is deprecated use renderText and renderHTML instead')
return this.options.renderLabel(args)
}
return this.options.renderText(args)
},
addKeyboardShortcuts() {
return {
Backspace: () =>
this.editor.commands.command(({ tr, state }) => {
let isMention = false
const { selection } = state
const { empty, anchor } = selection
if (!empty) {
return false
}
// Store node and position for later use
let mentionNode = new PMNode()
let mentionPos = 0
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
if (node.type.name === this.name) {
isMention = true
mentionNode = node
mentionPos = pos
return false
}
})
if (isMention) {
tr.insertText(
this.options.deleteTriggerWithBackspace
? ''
: mentionNode.attrs.mentionSuggestionChar,
mentionPos,
mentionPos + mentionNode.nodeSize,
)
}
return isMention
}),
}
},
addProseMirrorPlugins() {
// Create a plugin for each suggestion configuration
return getSuggestions(this).map(Suggestion)
},
})

View File

@@ -0,0 +1,89 @@
import type { Editor } from '@tiptap/core'
import { PluginKey } from '@tiptap/pm/state'
import type { SuggestionOptions } from '@tiptap/suggestion'
/**
* Arguments for the `getSuggestionOptions` function
* @see getSuggestionOptions
*/
export interface GetSuggestionOptionsOptions {
/**
* The Tiptap editor instance.
*/
editor: Editor
/**
* The suggestion options configuration provided to the
* `Mention` extension.
*/
overrideSuggestionOptions: Omit<SuggestionOptions, 'editor'>
/**
* The name of the Mention extension
*/
extensionName: string
/**
* The character that triggers the suggestion.
* @default '@'
*/
char?: string
}
/**
* Returns the suggestion options for a trigger of the Mention extension. These
* options are used to create a `Suggestion` ProseMirror plugin. Each plugin lets
* you define a different trigger that opens the Mention menu. For example,
* you can define a `@` trigger to mention users and a `#` trigger to mention
* tags.
*
* @param param0 The configured options for the suggestion
* @returns
*/
export function getSuggestionOptions({
editor: tiptapEditor,
overrideSuggestionOptions,
extensionName,
char = '@',
}: GetSuggestionOptionsOptions): SuggestionOptions {
const pluginKey = new PluginKey()
return {
editor: tiptapEditor,
char,
pluginKey,
command: ({ editor, range, props }: { editor: any; range: any; props: any }) => {
// increase range.to by one when the next node is of type "text"
// and starts with a space character
const nodeAfter = editor.view.state.selection.$to.nodeAfter
const overrideSpace = nodeAfter?.text?.startsWith(' ')
if (overrideSpace) {
range.to += 1
}
editor
.chain()
.focus()
.insertContentAt(range, [
{
type: extensionName,
attrs: { ...props, mentionSuggestionChar: char },
},
{
type: 'text',
text: ' ',
},
])
.run()
// get reference to `window` object from editor element, to support cross-frame JS usage
editor.view.dom.ownerDocument.defaultView?.getSelection()?.collapseToEnd()
},
allow: ({ state, range }: { state: any; range: any }) => {
const $from = state.doc.resolve(range.from)
const type = state.schema.nodes[extensionName]
const allow = !!$from.parent.type.contentMatch.matchType(type)
return allow
},
...overrideSuggestionOptions,
}
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite-plus'
import { basePackConfig } from '../../pack.config.mjs'
export default defineConfig({
pack: {
entry: ['src/index.ts'],
...basePackConfig(),
},
})