@tiptap/core 3.21.0 → 3.22.0
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/dist/index.cjs +21 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -3
- package/dist/index.d.ts +22 -3
- package/dist/index.js +19 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/htmlEntities.test.ts +55 -0
- package/src/commands/extendMarkRange.ts +3 -2
- package/src/extensions/delete.ts +2 -1
- package/src/helpers/getMarkRange.ts +7 -2
- package/src/lib/ResizableNodeView.ts +1 -1
- package/src/utilities/htmlEntities.ts +26 -0
- package/src/utilities/index.ts +1 -0
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.
|
|
4
|
+
"version": "3.22.0",
|
|
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.
|
|
55
|
+
"@tiptap/pm": "^3.22.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@tiptap/pm": "^3.
|
|
58
|
+
"@tiptap/pm": "^3.22.0"
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { decodeHtmlEntities, encodeHtmlEntities } from '../utilities/htmlEntities.js'
|
|
4
|
+
|
|
5
|
+
describe('decodeHtmlEntities', () => {
|
|
6
|
+
it('decodes < to <', () => {
|
|
7
|
+
expect(decodeHtmlEntities('<div>')).toBe('<div>')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('decodes & to &', () => {
|
|
11
|
+
expect(decodeHtmlEntities('a & b')).toBe('a & b')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('decodes " to "', () => {
|
|
15
|
+
expect(decodeHtmlEntities('"hello"')).toBe('"hello"')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('handles doubly-encoded sequences like &lt;', () => {
|
|
19
|
+
expect(decodeHtmlEntities('&lt;')).toBe('<')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns plain text unchanged', () => {
|
|
23
|
+
expect(decodeHtmlEntities('hello world')).toBe('hello world')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('encodeHtmlEntities', () => {
|
|
28
|
+
it('encodes < to <', () => {
|
|
29
|
+
expect(encodeHtmlEntities('<div>')).toBe('<div>')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('encodes & to &', () => {
|
|
33
|
+
expect(encodeHtmlEntities('a & b')).toBe('a & b')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('does not encode " (quotes are valid in markdown)', () => {
|
|
37
|
+
expect(encodeHtmlEntities('"hello"')).toBe('"hello"')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns plain text unchanged', () => {
|
|
41
|
+
expect(encodeHtmlEntities('hello world')).toBe('hello world')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('roundtrip', () => {
|
|
46
|
+
it.each(['<div>', 'a & b', 'x < y & y > z'])('encode then decode roundtrips: %s', input => {
|
|
47
|
+
expect(decodeHtmlEntities(encodeHtmlEntities(input))).toBe(input)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('decode is a superset of encode – " decodes but " is not encoded', () => {
|
|
51
|
+
// " passes through encode unchanged, " decodes to "
|
|
52
|
+
expect(encodeHtmlEntities('"hello"')).toBe('"hello"')
|
|
53
|
+
expect(decodeHtmlEntities('"hello"')).toBe('"hello"')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -11,7 +11,8 @@ declare module '@tiptap/core' {
|
|
|
11
11
|
/**
|
|
12
12
|
* Extends the text selection to the current mark by type or name.
|
|
13
13
|
* @param typeOrName The type or name of the mark.
|
|
14
|
-
* @param attributes The attributes
|
|
14
|
+
* @param attributes The attributes to match against.
|
|
15
|
+
* If not provided, only the first mark at the position will be matched.
|
|
15
16
|
* @example editor.commands.extendMarkRange('bold')
|
|
16
17
|
* @example editor.commands.extendMarkRange('mention', { userId: "1" })
|
|
17
18
|
*/
|
|
@@ -31,7 +32,7 @@ declare module '@tiptap/core' {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export const extendMarkRange: RawCommands['extendMarkRange'] =
|
|
34
|
-
(typeOrName, attributes
|
|
35
|
+
(typeOrName, attributes) =>
|
|
35
36
|
({ tr, state, dispatch }) => {
|
|
36
37
|
const type = getMarkType(typeOrName, state.schema)
|
|
37
38
|
const { doc, selection } = tr
|
package/src/extensions/delete.ts
CHANGED
|
@@ -55,7 +55,8 @@ export const Delete = Extension.create({
|
|
|
55
55
|
const oldStart = mapping.invert().map(newStart, -1)
|
|
56
56
|
const oldEnd = mapping.invert().map(newEnd)
|
|
57
57
|
|
|
58
|
-
const foundBeforeMark =
|
|
58
|
+
const foundBeforeMark =
|
|
59
|
+
newStart > 0 ? nextTransaction.doc.nodeAt(newStart - 1)?.marks.some(mark => mark.eq(step.mark)) : false
|
|
59
60
|
const foundAfterMark = nextTransaction.doc.nodeAt(newEnd)?.marks.some(mark => mark.eq(step.mark))
|
|
60
61
|
|
|
61
62
|
this.editor.emit('delete', {
|
|
@@ -57,8 +57,13 @@ export function getMarkRange(
|
|
|
57
57
|
return
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Default to only matching against the first mark
|
|
61
|
-
|
|
60
|
+
// Default to only matching against the attributes of the first mark with the given type
|
|
61
|
+
if (!attributes) {
|
|
62
|
+
const firstMark = start.node.marks.find(mark => mark.type === type)
|
|
63
|
+
if (firstMark) {
|
|
64
|
+
attributes = firstMark.attrs
|
|
65
|
+
}
|
|
66
|
+
}
|
|
62
67
|
|
|
63
68
|
// We now know that the cursor is either at the start, middle or end of a text node with the specified mark
|
|
64
69
|
// so we can look it up on the targeted mark
|
|
@@ -548,7 +548,7 @@ export class ResizableNodeView {
|
|
|
548
548
|
const element = document.createElement('div')
|
|
549
549
|
element.dataset.resizeContainer = ''
|
|
550
550
|
element.dataset.node = this.node.type.name
|
|
551
|
-
element.style.display = 'flex'
|
|
551
|
+
element.style.display = this.node.type.isInline ? 'inline-flex' : 'flex'
|
|
552
552
|
|
|
553
553
|
if (this.classNames.container) {
|
|
554
554
|
element.className = this.classNames.container
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode common HTML entities in text content so they display as literal
|
|
3
|
+
* characters inside the editor. The decode order matters: `&` must be
|
|
4
|
+
* decoded **last** so that doubly-encoded sequences like `&lt;` first
|
|
5
|
+
* survive the `<` pass and then correctly become `<` (not `<`).
|
|
6
|
+
*/
|
|
7
|
+
export function decodeHtmlEntities(text: string): string {
|
|
8
|
+
return text
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Encode HTML special characters so they roundtrip safely through markdown.
|
|
17
|
+
* `&` is encoded **first** to avoid double-encoding the ampersand in other
|
|
18
|
+
* entities (e.g. `<` → `<`, not `&lt;`).
|
|
19
|
+
*
|
|
20
|
+
* Note: `"` is intentionally NOT encoded here because double quotes are
|
|
21
|
+
* ordinary characters in markdown and do not need escaping. The decode
|
|
22
|
+
* function still handles `"` because the markdown tokenizer may emit it.
|
|
23
|
+
*/
|
|
24
|
+
export function encodeHtmlEntities(text: string): string {
|
|
25
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
26
|
+
}
|
package/src/utilities/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './elementFromString.js'
|
|
|
6
6
|
export * from './escapeForRegEx.js'
|
|
7
7
|
export * from './findDuplicates.js'
|
|
8
8
|
export * from './fromString.js'
|
|
9
|
+
export * from './htmlEntities.js'
|
|
9
10
|
export * from './isAndroid.js'
|
|
10
11
|
export * from './isEmptyObject.js'
|
|
11
12
|
export * from './isFirefox.js'
|