@tiptap/core 3.6.7 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/core",
3
3
  "description": "headless rich text editor",
4
- "version": "3.6.7",
4
+ "version": "3.7.1",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -52,10 +52,10 @@
52
52
  "jsx-dev-runtime"
53
53
  ],
54
54
  "devDependencies": {
55
- "@tiptap/pm": "^3.6.7"
55
+ "@tiptap/pm": "^3.7.1"
56
56
  },
57
57
  "peerDependencies": {
58
- "@tiptap/pm": "^3.6.7"
58
+ "@tiptap/pm": "^3.7.1"
59
59
  },
60
60
  "repository": {
61
61
  "type": "git",
package/src/Extendable.ts CHANGED
@@ -12,9 +12,16 @@ import type {
12
12
  EditorEvents,
13
13
  Extensions,
14
14
  GlobalAttributes,
15
+ JSONContent,
15
16
  KeyboardShortcutCommand,
17
+ MarkdownParseHelpers,
18
+ MarkdownParseResult,
19
+ MarkdownRendererHelpers,
20
+ MarkdownToken,
21
+ MarkdownTokenizer,
16
22
  ParentConfig,
17
23
  RawCommands,
24
+ RenderContext,
18
25
  } from './types.js'
19
26
  import { callOrReturn } from './utilities/callOrReturn.js'
20
27
  import { mergeDeep } from './utilities/mergeDeep.js'
@@ -224,6 +231,43 @@ export interface ExtendableConfig<
224
231
  parent: ParentConfig<Config>['addExtensions']
225
232
  }) => Extensions
226
233
 
234
+ /**
235
+ * The markdown token name
236
+ *
237
+ * This is the name of the token that this extension uses to parse and render markdown and comes from the Marked Lexer.
238
+ *
239
+ * @see https://github.com/markedjs/marked/blob/master/src/Tokens.ts
240
+ *
241
+ */
242
+ markdownTokenName?: string
243
+
244
+ /**
245
+ * The parse function used by the markdown parser to convert markdown tokens to ProseMirror nodes.
246
+ */
247
+ parseMarkdown?: (token: MarkdownToken, helpers: MarkdownParseHelpers) => MarkdownParseResult
248
+
249
+ /**
250
+ * The serializer function used by the markdown serializer to convert ProseMirror nodes to markdown tokens.
251
+ */
252
+ renderMarkdown?: (node: JSONContent, helpers: MarkdownRendererHelpers, ctx: RenderContext) => string
253
+
254
+ /**
255
+ * The markdown tokenizer responsible for turning a markdown string into tokens
256
+ *
257
+ * Custom tokenizers are only needed when you want to parse non-standard markdown token.
258
+ */
259
+ markdownTokenizer?: MarkdownTokenizer
260
+
261
+ /**
262
+ * Optional markdown options for indentation
263
+ */
264
+ markdownOptions?: {
265
+ /**
266
+ * Defines if this markdown element should indent it's child elements
267
+ */
268
+ indentsContent?: boolean
269
+ }
270
+
227
271
  /**
228
272
  * This function extends the schema of the node.
229
273
  * @example
@@ -29,12 +29,21 @@ export class ExtensionManager {
29
29
 
30
30
  schema: Schema
31
31
 
32
+ /**
33
+ * A flattened and sorted array of all extensions
34
+ */
32
35
  extensions: Extensions
33
36
 
37
+ /**
38
+ * A non-flattened array of base extensions (no sub-extensions)
39
+ */
40
+ baseExtensions: Extensions
41
+
34
42
  splittableMarks: string[] = []
35
43
 
36
44
  constructor(extensions: Extensions, editor: Editor) {
37
45
  this.editor = editor
46
+ this.baseExtensions = extensions
38
47
  this.extensions = resolveExtensions(extensions)
39
48
  this.schema = getSchemaByResolvedExtensions(this.extensions, editor)
40
49
  this.setupExtensions()
@@ -2,6 +2,20 @@ import type { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm
2
2
 
3
3
  import type { Content, RawCommands } from '../types.js'
4
4
 
5
+ export interface InsertContentOptions {
6
+ /**
7
+ * Options for parsing the content.
8
+ */
9
+ parseOptions?: ParseOptions
10
+
11
+ /**
12
+ * Whether to update the selection after inserting the content.
13
+ */
14
+ updateSelection?: boolean
15
+ applyInputRules?: boolean
16
+ applyPasteRules?: boolean
17
+ }
18
+
5
19
  declare module '@tiptap/core' {
6
20
  interface Commands<ReturnType> {
7
21
  insertContent: {
@@ -19,19 +33,7 @@ declare module '@tiptap/core' {
19
33
  /**
20
34
  * Optional options
21
35
  */
22
- options?: {
23
- /**
24
- * Options for parsing the content.
25
- */
26
- parseOptions?: ParseOptions
27
-
28
- /**
29
- * Whether to update the selection after inserting the content.
30
- */
31
- updateSelection?: boolean
32
- applyInputRules?: boolean
33
- applyPasteRules?: boolean
34
- },
36
+ options?: InsertContentOptions,
35
37
  ) => ReturnType
36
38
  }
37
39
  }
@@ -5,6 +5,33 @@ import { createNodeFromContent } from '../helpers/createNodeFromContent.js'
5
5
  import { selectionToInsertionEnd } from '../helpers/selectionToInsertionEnd.js'
6
6
  import type { Content, Range, RawCommands } from '../types.js'
7
7
 
8
+ export interface InsertContentAtOptions {
9
+ /**
10
+ * Options for parsing the content.
11
+ */
12
+ parseOptions?: ParseOptions
13
+
14
+ /**
15
+ * Whether to update the selection after inserting the content.
16
+ */
17
+ updateSelection?: boolean
18
+
19
+ /**
20
+ * Whether to apply input rules after inserting the content.
21
+ */
22
+ applyInputRules?: boolean
23
+
24
+ /**
25
+ * Whether to apply paste rules after inserting the content.
26
+ */
27
+ applyPasteRules?: boolean
28
+
29
+ /**
30
+ * Whether to throw an error if the content is invalid.
31
+ */
32
+ errorOnInvalidContent?: boolean
33
+ }
34
+
8
35
  declare module '@tiptap/core' {
9
36
  interface Commands<ReturnType> {
10
37
  insertContentAt: {
@@ -26,32 +53,7 @@ declare module '@tiptap/core' {
26
53
  /**
27
54
  * Optional options
28
55
  */
29
- options?: {
30
- /**
31
- * Options for parsing the content.
32
- */
33
- parseOptions?: ParseOptions
34
-
35
- /**
36
- * Whether to update the selection after inserting the content.
37
- */
38
- updateSelection?: boolean
39
-
40
- /**
41
- * Whether to apply input rules after inserting the content.
42
- */
43
- applyInputRules?: boolean
44
-
45
- /**
46
- * Whether to apply paste rules after inserting the content.
47
- */
48
- applyPasteRules?: boolean
49
-
50
- /**
51
- * Whether to throw an error if the content is invalid.
52
- */
53
- errorOnInvalidContent?: boolean
54
- },
56
+ options?: InsertContentAtOptions,
55
57
  ) => ReturnType
56
58
  }
57
59
  }
@@ -3,6 +3,25 @@ import type { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm
3
3
  import { createDocument } from '../helpers/createDocument.js'
4
4
  import type { Content, RawCommands } from '../types.js'
5
5
 
6
+ export interface SetContentOptions {
7
+ /**
8
+ * Options for parsing the content.
9
+ * @default {}
10
+ */
11
+ parseOptions?: ParseOptions
12
+
13
+ /**
14
+ * Whether to throw an error if the content is invalid.
15
+ */
16
+ errorOnInvalidContent?: boolean
17
+
18
+ /**
19
+ * Whether to emit an update event.
20
+ * @default true
21
+ */
22
+ emitUpdate?: boolean
23
+ }
24
+
6
25
  declare module '@tiptap/core' {
7
26
  interface Commands<ReturnType> {
8
27
  setContent: {
@@ -22,24 +41,7 @@ declare module '@tiptap/core' {
22
41
  /**
23
42
  * Options for `setContent`.
24
43
  */
25
- options?: {
26
- /**
27
- * Options for parsing the content.
28
- * @default {}
29
- */
30
- parseOptions?: ParseOptions
31
-
32
- /**
33
- * Whether to throw an error if the content is invalid.
34
- */
35
- errorOnInvalidContent?: boolean
36
-
37
- /**
38
- * Whether to emit an update event.
39
- * @default true
40
- */
41
- emitUpdate?: boolean
42
- },
44
+ options?: SetContentOptions,
43
45
  ) => ReturnType
44
46
  }
45
47
  }
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export * from './CommandManager.js'
2
+ export type * from './commands/index.js'
3
+ export * as commands from './commands/index.js'
2
4
  export * from './Editor.js'
5
+ export * from './Extendable.js'
3
6
  export * from './Extension.js'
4
7
  export * as extensions from './extensions/index.js'
5
8
  export * from './helpers/index.js'
@@ -8,7 +8,7 @@ import { callOrReturn } from '../utilities/index.js'
8
8
  /**
9
9
  * Build an paste rule that adds a node when the
10
10
  * matched text is pasted into it.
11
- * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
11
+ * @see https://tiptap.dev/docs/editor/api/paste-rules
12
12
  */
13
13
  export function nodePasteRule(config: {
14
14
  find: PasteRuleFinder
package/src/types.ts CHANGED
@@ -755,3 +755,159 @@ export type ExtendedRegExpMatchArray = RegExpMatchArray & {
755
755
  }
756
756
 
757
757
  export type Dispatch = ((args?: any) => any) | undefined
758
+
759
+ /** Markdown related types */
760
+
761
+ // Shared markdown-related types for the MarkdownManager and extensions.
762
+ export type MarkdownToken = {
763
+ type?: string
764
+ raw?: string
765
+ text?: string
766
+ tokens?: MarkdownToken[]
767
+ depth?: number
768
+ items?: MarkdownToken[]
769
+ [key: string]: any
770
+ }
771
+
772
+ export type MarkdownHelpers = {
773
+ // When used during parsing these helpers return JSON-like node objects
774
+ // (not ProseMirror Node instances). Use `any` to represent that shape.
775
+ parseInline: (tokens: MarkdownToken[]) => any[]
776
+ /**
777
+ * Render children. The second argument may be a legacy separator string
778
+ * or a RenderContext (preferred).
779
+ */
780
+ renderChildren: (node: Node[] | Node, ctxOrSeparator?: RenderContext | string) => string
781
+ text: (token: MarkdownToken) => any
782
+ }
783
+
784
+ /**
785
+ * Helpers specifically for parsing markdown tokens into Tiptap JSON.
786
+ * These are provided to extension parse handlers.
787
+ */
788
+ export type MarkdownParseHelpers = {
789
+ /** Parse an array of inline tokens into text nodes with marks */
790
+ parseInline: (tokens: MarkdownToken[]) => JSONContent[]
791
+ /** Parse an array of block-level tokens */
792
+ parseChildren: (tokens: MarkdownToken[]) => JSONContent[]
793
+ /** Create a text node with optional marks */
794
+ createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => JSONContent
795
+ /** Create any node type with attributes and content */
796
+ createNode: (type: string, attrs?: any, content?: JSONContent[]) => JSONContent
797
+ /** Apply a mark to content (used for inline marks like bold, italic) */
798
+ applyMark: (
799
+ markType: string,
800
+ content: JSONContent[],
801
+ attrs?: any,
802
+ ) => { mark: string; content: JSONContent[]; attrs?: any }
803
+ }
804
+
805
+ /**
806
+ * Full runtime helpers object provided by MarkdownManager to handlers.
807
+ * This includes the small author-facing helpers plus internal helpers
808
+ * that can be useful for advanced handlers.
809
+ */
810
+ export type FullMarkdownHelpers = MarkdownHelpers & {
811
+ // parseChildren returns JSON-like nodes when invoked during parsing.
812
+ parseChildren: (tokens: MarkdownToken[]) => any[]
813
+ getExtension: (name: string) => any
814
+ // createNode returns a JSON-like node during parsing; render-time helpers
815
+ // may instead work with real ProseMirror Node instances.
816
+ createNode: (type: string, attrs?: any, content?: any[]) => any
817
+ /** Current render context when calling renderers; undefined during parse. */
818
+ currentContext?: RenderContext
819
+ /** Indent a multi-line string according to the provided RenderContext. */
820
+ indent: (text: string, ctx?: RenderContext) => string
821
+ /** Return the indent string for a given level (e.g. ' ' or '\t'). */
822
+ getIndentString: (level?: number) => string
823
+ }
824
+
825
+ export default MarkdownHelpers
826
+
827
+ /**
828
+ * Return shape for parser-level `parse` handlers.
829
+ * - a single JSON-like node
830
+ * - an array of JSON-like nodes
831
+ * - or a `{ mark: string, content: JSONLike[] }` shape to apply a mark
832
+ */
833
+ export type MarkdownParseResult = JSONContent | JSONContent[] | { mark: string; content: JSONContent[]; attrs?: any }
834
+
835
+ export type RenderContext = {
836
+ index: number
837
+ level: number
838
+ meta?: Record<string, any>
839
+ parentType?: string | null
840
+ }
841
+
842
+ /** Extension contract for markdown parsing/serialization. */
843
+ export interface MarkdownExtensionSpec {
844
+ /** Token name used for parsing (e.g., 'codespan', 'code', 'strong') */
845
+ tokenName?: string
846
+ /** Node/mark name used for rendering (typically the extension name) */
847
+ nodeName?: string
848
+ parseMarkdown?: (token: MarkdownToken, helpers: MarkdownParseHelpers) => MarkdownParseResult
849
+ renderMarkdown?: (node: any, helpers: MarkdownRendererHelpers, ctx: RenderContext) => string
850
+ isIndenting?: boolean
851
+ /** Custom tokenizer for marked.js to handle non-standard markdown syntax */
852
+ tokenizer?: MarkdownTokenizer
853
+ }
854
+
855
+ /**
856
+ * Configuration object passed to custom marked.js tokenizers
857
+ */
858
+ export type MarkdownLexerConfiguration = {
859
+ /**
860
+ * Can be used to transform source text into inline tokens - useful while tokenizing child tokens.
861
+ * @param src
862
+ * @returns Array of inline tokens
863
+ */
864
+ inlineTokens: (src: string) => MarkdownToken[]
865
+
866
+ /**
867
+ * Can be used to transform source text into block-level tokens - useful while tokenizing child tokens.
868
+ * @param src
869
+ * @returns Array of block-level tokens
870
+ */
871
+ blockTokens: (src: string) => MarkdownToken[]
872
+ }
873
+
874
+ /** Custom tokenizer function for marked.js extensions */
875
+ export type MarkdownTokenizer = {
876
+ /** Token name this tokenizer creates */
877
+ name: string
878
+ /** Priority level for tokenizer ordering (higher = earlier) */
879
+ level?: 'block' | 'inline'
880
+ /** A string to look for or a function that returns the start index of the token in the source string */
881
+ start?: string | ((src: string) => number)
882
+ /** Function that attempts to parse custom syntax from start of text */
883
+ tokenize: (
884
+ src: string,
885
+ tokens: MarkdownToken[],
886
+ lexer: MarkdownLexerConfiguration,
887
+ ) => MarkdownToken | undefined | void
888
+ }
889
+
890
+ export type MarkdownRendererHelpers = {
891
+ /**
892
+ * Render children nodes to a markdown string, optionally separated by a string.
893
+ * @param nodes The node or array of nodes to render
894
+ * @param separator An optional separator string (legacy) or RenderContext
895
+ * @returns The rendered markdown string
896
+ */
897
+ renderChildren: (nodes: JSONContent | JSONContent[], separator?: string) => string
898
+
899
+ /**
900
+ * Render a text token to a markdown string
901
+ * @param prefix The prefix to add before the content
902
+ * @param content The content to wrap
903
+ * @returns The wrapped content
904
+ */
905
+ wrapInBlock: (prefix: string, content: string) => string
906
+
907
+ /**
908
+ * Indent a markdown string according to the provided RenderContext
909
+ * @param content The content to indent
910
+ * @returns The indented content
911
+ */
912
+ indent: (content: string) => string
913
+ }
@@ -15,6 +15,8 @@ export * from './isNumber.js'
15
15
  export * from './isPlainObject.js'
16
16
  export * from './isRegExp.js'
17
17
  export * from './isString.js'
18
+ export * from './markdown/index.js'
19
+ export * as markdown from './markdown/index.js'
18
20
  export * from './mergeAttributes.js'
19
21
  export * from './mergeDeep.js'
20
22
  export * from './minMax.js'
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @fileoverview Utility functions for parsing and serializing markdown attributes.
3
+ *
4
+ * These utilities handle the common patterns for parsing attribute strings
5
+ * in various markdown syntaxes like Pandoc attributes.
6
+ */
7
+
8
+ /**
9
+ * Parses a Pandoc-style attribute string into an object.
10
+ *
11
+ * Supports the following patterns:
12
+ * - Classes: `.className` → `{ class: 'className' }`
13
+ * - IDs: `#myId` → `{ id: 'myId' }`
14
+ * - Key-value pairs: `key="value"` → `{ key: 'value' }`
15
+ * - Boolean attributes: `disabled` → `{ disabled: true }`
16
+ *
17
+ * @param attrString - The attribute string to parse
18
+ * @returns Parsed attributes object
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * parseAttributes('.btn #submit disabled type="button"')
23
+ * // → { class: 'btn', id: 'submit', disabled: true, type: 'button' }
24
+ * ```
25
+ */
26
+ export function parseAttributes(attrString: string): Record<string, any> {
27
+ if (!attrString?.trim()) {
28
+ return {}
29
+ }
30
+
31
+ const attributes: Record<string, any> = {}
32
+
33
+ // First, extract and remove quoted strings to avoid parsing content inside them
34
+ const quotedStrings: string[] = []
35
+ const tempString = attrString.replace(/["']([^"']*)["']/g, match => {
36
+ quotedStrings.push(match)
37
+ return `__QUOTED_${quotedStrings.length - 1}__`
38
+ })
39
+
40
+ // Parse classes (.className) - only outside of quoted strings
41
+ const classMatches = tempString.match(/(?:^|\s)\.([a-zA-Z][\w-]*)/g)
42
+ if (classMatches) {
43
+ const classes = classMatches.map(match => match.trim().slice(1)) // Remove the dot
44
+ attributes.class = classes.join(' ')
45
+ }
46
+
47
+ // Parse IDs (#myId) - only outside of quoted strings
48
+ const idMatch = tempString.match(/(?:^|\s)#([a-zA-Z][\w-]*)/)
49
+ if (idMatch) {
50
+ attributes.id = idMatch[1]
51
+ }
52
+
53
+ // Parse key-value pairs (key="value" or key='value') - restore quoted strings
54
+ const kvRegex = /([a-zA-Z][\w-]*)\s*=\s*(__QUOTED_\d+__)/g
55
+ const kvMatches = Array.from(tempString.matchAll(kvRegex))
56
+ kvMatches.forEach(([, key, quotedRef]) => {
57
+ const quotedIndex = parseInt(quotedRef.match(/__QUOTED_(\d+)__/)?.[1] || '0', 10)
58
+ const quotedValue = quotedStrings[quotedIndex]
59
+ if (quotedValue) {
60
+ // Remove the outer quotes
61
+ attributes[key] = quotedValue.slice(1, -1)
62
+ }
63
+ })
64
+
65
+ // Parse boolean attributes (standalone words that aren't classes/IDs)
66
+ const cleanString = tempString
67
+ .replace(/(?:^|\s)\.([a-zA-Z][\w-]*)/g, '') // Remove classes
68
+ .replace(/(?:^|\s)#([a-zA-Z][\w-]*)/g, '') // Remove IDs
69
+ .replace(/([a-zA-Z][\w-]*)\s*=\s*__QUOTED_\d+__/g, '') // Remove key-value pairs
70
+ .trim()
71
+
72
+ if (cleanString) {
73
+ const booleanAttrs = cleanString.split(/\s+/).filter(Boolean)
74
+ booleanAttrs.forEach(attr => {
75
+ if (attr.match(/^[a-zA-Z][\w-]*$/)) {
76
+ attributes[attr] = true
77
+ }
78
+ })
79
+ }
80
+
81
+ return attributes
82
+ }
83
+
84
+ /**
85
+ * Serializes an attributes object back to a Pandoc-style attribute string.
86
+ *
87
+ * @param attributes - The attributes object to serialize
88
+ * @returns Serialized attribute string
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * serializeAttributes({ class: 'btn primary', id: 'submit', disabled: true, type: 'button' })
93
+ * // → '.btn.primary #submit disabled type="button"'
94
+ * ```
95
+ */
96
+ export function serializeAttributes(attributes: Record<string, any>): string {
97
+ if (!attributes || Object.keys(attributes).length === 0) {
98
+ return ''
99
+ }
100
+
101
+ const parts: string[] = []
102
+
103
+ // Handle classes
104
+ if (attributes.class) {
105
+ const classes = String(attributes.class).split(/\s+/).filter(Boolean)
106
+ classes.forEach(cls => parts.push(`.${cls}`))
107
+ }
108
+
109
+ // Handle ID
110
+ if (attributes.id) {
111
+ parts.push(`#${attributes.id}`)
112
+ }
113
+
114
+ // Handle other attributes
115
+ Object.entries(attributes).forEach(([key, value]) => {
116
+ if (key === 'class' || key === 'id') {
117
+ return // Already handled
118
+ }
119
+
120
+ if (value === true) {
121
+ // Boolean attribute
122
+ parts.push(key)
123
+ } else if (value !== false && value != null) {
124
+ // Key-value attribute
125
+ parts.push(`${key}="${String(value)}"`)
126
+ }
127
+ })
128
+
129
+ return parts.join(' ')
130
+ }