@markup-carve/carve-grammars 0.1.2
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/LICENSE +21 -0
- package/README.md +240 -0
- package/highlightjs/carve.js +537 -0
- package/highlightjs/carve.mjs +25 -0
- package/package.json +119 -0
- package/prism/carve.js +471 -0
- package/shiki/carve.css +46 -0
- package/shiki/index.js +188 -0
- package/textmate/carve.tmLanguage.json +1023 -0
- package/tiptap/carve-kit.js +507 -0
- package/tiptap/extensions/carve-abbreviation.js +79 -0
- package/tiptap/extensions/carve-definition-list.js +270 -0
- package/tiptap/extensions/carve-delete.js +54 -0
- package/tiptap/extensions/carve-div.js +188 -0
- package/tiptap/extensions/carve-embed.js +248 -0
- package/tiptap/extensions/carve-footnote-definition.js +81 -0
- package/tiptap/extensions/carve-footnote.js +76 -0
- package/tiptap/extensions/carve-insert.js +54 -0
- package/tiptap/extensions/carve-keymap.js +71 -0
- package/tiptap/extensions/carve-math.js +82 -0
- package/tiptap/extensions/carve-mention.js +44 -0
- package/tiptap/extensions/carve-span.js +101 -0
- package/tiptap/extensions/carve-tabs.js +158 -0
- package/tiptap/extensions/index.js +14 -0
- package/tiptap/index.js +53 -0
- package/tiptap/serializer.js +686 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carve Definition List extension for Tiptap
|
|
5
|
+
*
|
|
6
|
+
* Provides full definition list support with keyboard navigation.
|
|
7
|
+
*
|
|
8
|
+
* Carve syntax:
|
|
9
|
+
* ```
|
|
10
|
+
* : term
|
|
11
|
+
*
|
|
12
|
+
* Definition paragraph (indented)
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Multiple terms sharing definition:
|
|
16
|
+
* ```
|
|
17
|
+
* : color
|
|
18
|
+
* : colour
|
|
19
|
+
*
|
|
20
|
+
* The visual property...
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Multiple definitions (: + continuation):
|
|
24
|
+
* ```
|
|
25
|
+
* : term
|
|
26
|
+
*
|
|
27
|
+
* First definition.
|
|
28
|
+
*
|
|
29
|
+
* : +
|
|
30
|
+
*
|
|
31
|
+
* Second definition.
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Keyboard shortcuts:
|
|
35
|
+
* - Enter in term: go to definition
|
|
36
|
+
* - Shift+Enter in term: add another term (for synonyms)
|
|
37
|
+
* - Enter in empty definition: create new term+definition pair
|
|
38
|
+
* - Shift+Enter in definition: add another definition (: + syntax)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Definition List container node (<dl>)
|
|
43
|
+
*/
|
|
44
|
+
export const CarveDefinitionList = Node.create({
|
|
45
|
+
name: 'definitionList',
|
|
46
|
+
|
|
47
|
+
group: 'block',
|
|
48
|
+
|
|
49
|
+
// Allow multiple terms followed by multiple descriptions, repeating
|
|
50
|
+
content: '(definitionTerm+ definitionDescription+)+',
|
|
51
|
+
|
|
52
|
+
parseHTML() {
|
|
53
|
+
return [{ tag: 'dl', priority: 51 }];
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
renderHTML({ HTMLAttributes }) {
|
|
57
|
+
return ['dl', mergeAttributes(HTMLAttributes, { class: 'carve-definition-list' }), 0];
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
addCommands() {
|
|
61
|
+
return {
|
|
62
|
+
insertDefinitionList: () => ({ chain, state }) => {
|
|
63
|
+
const { $from } = state.selection;
|
|
64
|
+
const insertPos = $from.end();
|
|
65
|
+
|
|
66
|
+
return chain()
|
|
67
|
+
.insertContentAt(insertPos, {
|
|
68
|
+
type: 'definitionList',
|
|
69
|
+
content: [
|
|
70
|
+
{ type: 'definitionTerm' },
|
|
71
|
+
{ type: 'definitionDescription', content: [{ type: 'paragraph' }] },
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
// Focus at the start of the term
|
|
75
|
+
.focus(insertPos + 2)
|
|
76
|
+
.run();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Definition Term node (<dt>)
|
|
84
|
+
* Note: No group - can only appear inside definitionList
|
|
85
|
+
*/
|
|
86
|
+
export const CarveDefinitionTerm = Node.create({
|
|
87
|
+
name: 'definitionTerm',
|
|
88
|
+
|
|
89
|
+
content: 'inline*',
|
|
90
|
+
|
|
91
|
+
defining: true,
|
|
92
|
+
|
|
93
|
+
parseHTML() {
|
|
94
|
+
return [{ tag: 'dt', priority: 51 }];
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
renderHTML({ HTMLAttributes }) {
|
|
98
|
+
return ['dt', mergeAttributes(HTMLAttributes), 0];
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
addKeyboardShortcuts() {
|
|
102
|
+
return {
|
|
103
|
+
// Enter in term: go to description or create one
|
|
104
|
+
'Enter': () => {
|
|
105
|
+
const { state } = this.editor;
|
|
106
|
+
const { $from } = state.selection;
|
|
107
|
+
|
|
108
|
+
if ($from.parent.type.name !== 'definitionTerm') {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const termEnd = $from.end();
|
|
113
|
+
|
|
114
|
+
// Empty term - let default behavior handle it
|
|
115
|
+
if ($from.parent.content.size === 0) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check what comes next
|
|
120
|
+
const $afterTerm = state.doc.resolve(termEnd + 1);
|
|
121
|
+
const nextNode = $afterTerm.nodeAfter;
|
|
122
|
+
|
|
123
|
+
if (nextNode && nextNode.type.name === 'definitionDescription') {
|
|
124
|
+
// Move cursor to the description
|
|
125
|
+
return this.editor.chain()
|
|
126
|
+
.focus(termEnd + 2)
|
|
127
|
+
.run();
|
|
128
|
+
} else {
|
|
129
|
+
// Insert a description after this term
|
|
130
|
+
return this.editor.chain()
|
|
131
|
+
.insertContentAt(termEnd + 1, {
|
|
132
|
+
type: 'definitionDescription',
|
|
133
|
+
content: [{ type: 'paragraph' }],
|
|
134
|
+
})
|
|
135
|
+
.focus(termEnd + 3)
|
|
136
|
+
.run();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
// Shift+Enter: add another term (for multiple terms sharing a definition)
|
|
140
|
+
'Shift-Enter': () => {
|
|
141
|
+
const { state } = this.editor;
|
|
142
|
+
const { $from } = state.selection;
|
|
143
|
+
|
|
144
|
+
if ($from.parent.type.name !== 'definitionTerm') {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const termEnd = $from.end();
|
|
149
|
+
|
|
150
|
+
return this.editor.chain()
|
|
151
|
+
.insertContentAt(termEnd + 1, {
|
|
152
|
+
type: 'definitionTerm',
|
|
153
|
+
})
|
|
154
|
+
.focus(termEnd + 2)
|
|
155
|
+
.run();
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Definition Description node (<dd>)
|
|
163
|
+
* Note: No group - can only appear inside definitionList
|
|
164
|
+
*/
|
|
165
|
+
export const CarveDefinitionDescription = Node.create({
|
|
166
|
+
name: 'definitionDescription',
|
|
167
|
+
|
|
168
|
+
content: 'block+',
|
|
169
|
+
|
|
170
|
+
defining: true,
|
|
171
|
+
|
|
172
|
+
parseHTML() {
|
|
173
|
+
return [{ tag: 'dd', priority: 51 }];
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
renderHTML({ HTMLAttributes }) {
|
|
177
|
+
return ['dd', mergeAttributes(HTMLAttributes), 0];
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
addKeyboardShortcuts() {
|
|
181
|
+
return {
|
|
182
|
+
// Enter in empty paragraph inside description: create new term+description pair
|
|
183
|
+
'Enter': () => {
|
|
184
|
+
const { state } = this.editor;
|
|
185
|
+
const { $from } = state.selection;
|
|
186
|
+
|
|
187
|
+
// Check if we're in a paragraph inside a description
|
|
188
|
+
let inDescription = false;
|
|
189
|
+
let descriptionDepth = 0;
|
|
190
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
191
|
+
if ($from.node(d).type.name === 'definitionDescription') {
|
|
192
|
+
inDescription = true;
|
|
193
|
+
descriptionDepth = d;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!inDescription) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if current paragraph is empty
|
|
203
|
+
const paragraph = $from.parent;
|
|
204
|
+
if (paragraph.type.name !== 'paragraph' || paragraph.content.size !== 0) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Find the definition list
|
|
209
|
+
let listDepth = 0;
|
|
210
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
211
|
+
if ($from.node(d).type.name === 'definitionList') {
|
|
212
|
+
listDepth = d;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (listDepth === 0) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const descNode = $from.node(descriptionDepth);
|
|
222
|
+
|
|
223
|
+
// If description only has one empty paragraph, add new term + description
|
|
224
|
+
if (descNode.childCount === 1) {
|
|
225
|
+
const descEnd = $from.end(descriptionDepth);
|
|
226
|
+
|
|
227
|
+
return this.editor.chain()
|
|
228
|
+
.insertContentAt(descEnd + 1, [
|
|
229
|
+
{ type: 'definitionTerm' },
|
|
230
|
+
{ type: 'definitionDescription', content: [{ type: 'paragraph' }] },
|
|
231
|
+
])
|
|
232
|
+
.focus(descEnd + 2)
|
|
233
|
+
.run();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
},
|
|
238
|
+
// Shift+Enter: add another description (for : + continuation)
|
|
239
|
+
'Shift-Enter': () => {
|
|
240
|
+
const { state } = this.editor;
|
|
241
|
+
const { $from } = state.selection;
|
|
242
|
+
|
|
243
|
+
// Check if we're in a description
|
|
244
|
+
let descriptionDepth = 0;
|
|
245
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
246
|
+
if ($from.node(d).type.name === 'definitionDescription') {
|
|
247
|
+
descriptionDepth = d;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (descriptionDepth === 0) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const descEnd = $from.end(descriptionDepth);
|
|
257
|
+
|
|
258
|
+
return this.editor.chain()
|
|
259
|
+
.insertContentAt(descEnd + 1, {
|
|
260
|
+
type: 'definitionDescription',
|
|
261
|
+
content: [{ type: 'paragraph' }],
|
|
262
|
+
})
|
|
263
|
+
.focus(descEnd + 3)
|
|
264
|
+
.run();
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
export default { CarveDefinitionList, CarveDefinitionTerm, CarveDefinitionDescription };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Mark, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carve Delete mark extension for Tiptap
|
|
5
|
+
*
|
|
6
|
+
* Renders as {-text-} in Carve markup
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* import { CarveDelete } from 'carve-grammars/tiptap'
|
|
11
|
+
*
|
|
12
|
+
* const editor = new Editor({
|
|
13
|
+
* extensions: [CarveDelete],
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* // Toggle delete mark
|
|
17
|
+
* editor.chain().focus().toggleCarveDelete().run()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const CarveDelete = Mark.create({
|
|
21
|
+
name: 'carveDelete',
|
|
22
|
+
|
|
23
|
+
// Outrank StarterKit's Strike, whose parseHTML also claims <del> - at
|
|
24
|
+
// default priority the strike mark wins and {-...-} degrades to ~...~
|
|
25
|
+
// after an HTML round-trip.
|
|
26
|
+
priority: 101,
|
|
27
|
+
|
|
28
|
+
parseHTML() {
|
|
29
|
+
return [
|
|
30
|
+
{ tag: 'del' },
|
|
31
|
+
{ tag: 'span.carve-delete' },
|
|
32
|
+
];
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
renderHTML({ HTMLAttributes }) {
|
|
36
|
+
return ['span', mergeAttributes(HTMLAttributes, { class: 'carve-delete' }), 0];
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
addCommands() {
|
|
40
|
+
return {
|
|
41
|
+
toggleCarveDelete: () => ({ commands }) => commands.toggleMark(this.name),
|
|
42
|
+
setCarveDelete: () => ({ commands }) => commands.setMark(this.name),
|
|
43
|
+
unsetCarveDelete: () => ({ commands }) => commands.unsetMark(this.name),
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
addKeyboardShortcuts() {
|
|
48
|
+
return {
|
|
49
|
+
'Mod-Shift-d': () => this.editor.commands.toggleCarveDelete(),
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export default CarveDelete;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* First direct child that carries the admonition-title class (carve-php and
|
|
5
|
+
* carve-js render a quoted container title as such a paragraph).
|
|
6
|
+
*/
|
|
7
|
+
function findTitleChild(element) {
|
|
8
|
+
for (const child of element.children || []) {
|
|
9
|
+
if (child.classList && child.classList.contains('admonition-title')) {
|
|
10
|
+
return child;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** First direct child carrying the carve-div-body class (own editor DOM). */
|
|
17
|
+
function findBodyChild(element) {
|
|
18
|
+
for (const child of element.children || []) {
|
|
19
|
+
if (child.classList && child.classList.contains('carve-div-body')) {
|
|
20
|
+
return child;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Content for the node: everything except the title paragraph, which is
|
|
28
|
+
* captured as the `title` attribute instead (else it would duplicate into the
|
|
29
|
+
* body and the quoted summary would be lost on serialization). The editor's
|
|
30
|
+
* own DOM wraps content in .carve-div-body next to the rendered title.
|
|
31
|
+
*/
|
|
32
|
+
function contentWithoutTitle(element) {
|
|
33
|
+
const body = findBodyChild(element);
|
|
34
|
+
if (body) {
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
const clone = element.cloneNode(true);
|
|
38
|
+
const title = findTitleChild(clone);
|
|
39
|
+
if (!title) {
|
|
40
|
+
return element;
|
|
41
|
+
}
|
|
42
|
+
title.remove();
|
|
43
|
+
return clone;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Carve Div container node extension for Tiptap
|
|
48
|
+
*
|
|
49
|
+
* Renders as ::: class in Carve markup, with an optional quoted title
|
|
50
|
+
* (::: note "Custom title") kept in the `title` attribute
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```js
|
|
54
|
+
* import { CarveDiv } from 'carve-grammars/tiptap'
|
|
55
|
+
*
|
|
56
|
+
* const editor = new Editor({
|
|
57
|
+
* extensions: [CarveDiv],
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* // Wrap selection in a div container
|
|
61
|
+
* editor.chain().focus().setCarveDiv({ class: 'warning' }).run()
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export const CarveDiv = Node.create({
|
|
65
|
+
name: 'carveDiv',
|
|
66
|
+
|
|
67
|
+
group: 'block',
|
|
68
|
+
|
|
69
|
+
content: 'block+',
|
|
70
|
+
|
|
71
|
+
defining: true,
|
|
72
|
+
|
|
73
|
+
addAttributes() {
|
|
74
|
+
return {
|
|
75
|
+
class: {
|
|
76
|
+
default: null,
|
|
77
|
+
parseHTML: element => element.getAttribute('data-carve-class')
|
|
78
|
+
// Drop the framing classes so an <aside class="admonition note">
|
|
79
|
+
// (carve-php / carve-js output) yields just "note".
|
|
80
|
+
|| element.className.replace(/\b(carve-div|admonition)\b/g, '').replace(/\s+/g, ' ').trim()
|
|
81
|
+
|| null,
|
|
82
|
+
renderHTML: attributes => {
|
|
83
|
+
if (!attributes.class) return {};
|
|
84
|
+
return { 'data-carve-class': attributes.class };
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
title: {
|
|
88
|
+
default: null,
|
|
89
|
+
// An empty string is meaningful (::: note "" suppresses the
|
|
90
|
+
// default title), so only a missing title maps to null.
|
|
91
|
+
parseHTML: element => {
|
|
92
|
+
const attr = element.getAttribute('data-carve-title');
|
|
93
|
+
if (attr !== null) return attr;
|
|
94
|
+
const child = findTitleChild(element);
|
|
95
|
+
if (child) return child.textContent.trim();
|
|
96
|
+
// carve-js renders an authored {title="..."} block attribute
|
|
97
|
+
// as a literal title attribute on the container (carve-php
|
|
98
|
+
// promotes it to an admonition-title paragraph instead);
|
|
99
|
+
// capture it so the title survives that engine's seed too.
|
|
100
|
+
return element.getAttribute('title');
|
|
101
|
+
},
|
|
102
|
+
renderHTML: attributes => {
|
|
103
|
+
if (attributes.title == null) return {};
|
|
104
|
+
return { 'data-carve-title': attributes.title };
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
parseHTML() {
|
|
111
|
+
return [
|
|
112
|
+
{ tag: 'div.carve-div', contentElement: contentWithoutTitle },
|
|
113
|
+
// Admonitions render as <aside class="admonition TYPE"> (carve-php,
|
|
114
|
+
// carve-js). Match highest so it wins over the generic rules.
|
|
115
|
+
{ tag: 'aside.admonition', priority: 60, contentElement: contentWithoutTitle },
|
|
116
|
+
// Also match common container classes rendered by carve-php
|
|
117
|
+
{ tag: 'div.note', contentElement: contentWithoutTitle },
|
|
118
|
+
{ tag: 'div.tip', contentElement: contentWithoutTitle },
|
|
119
|
+
{ tag: 'div.warning', contentElement: contentWithoutTitle },
|
|
120
|
+
{ tag: 'div.danger', contentElement: contentWithoutTitle },
|
|
121
|
+
{ tag: 'div.info', contentElement: contentWithoutTitle },
|
|
122
|
+
// Match any div with a single class (likely a ::: container)
|
|
123
|
+
{
|
|
124
|
+
tag: 'div[class]',
|
|
125
|
+
contentElement: contentWithoutTitle,
|
|
126
|
+
getAttrs: element => {
|
|
127
|
+
// Only match divs with a simple class (not complex component divs)
|
|
128
|
+
const className = element.className;
|
|
129
|
+
// Skip if it looks like a WordPress/editor component
|
|
130
|
+
if (className.includes('wp-') || className.includes('block-') ||
|
|
131
|
+
className.includes('editor-') || className.includes('is-')) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// Skip Torchlight code block line divs
|
|
135
|
+
if (className === 'line' || className.includes('line-')) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// Skip if inside a pre or code element (syntax highlighting)
|
|
139
|
+
if (element.closest('pre') || element.closest('code')) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
// Accept single-word classes or carve-div
|
|
143
|
+
if (/^[a-z-]+$/i.test(className) || className.includes('carve-div')) {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
renderHTML({ HTMLAttributes }) {
|
|
153
|
+
const classes = ['carve-div'];
|
|
154
|
+
if (HTMLAttributes['data-carve-class']) {
|
|
155
|
+
classes.push(HTMLAttributes['data-carve-class']);
|
|
156
|
+
}
|
|
157
|
+
const attrs = mergeAttributes(HTMLAttributes, { class: classes.join(' ') });
|
|
158
|
+
const title = HTMLAttributes['data-carve-title'];
|
|
159
|
+
if (title === undefined) {
|
|
160
|
+
return ['div', attrs, 0];
|
|
161
|
+
}
|
|
162
|
+
// Keep the captured title VISIBLE in the editor: a non-editable title
|
|
163
|
+
// element plus a body wrapper carrying the content hole (ProseMirror
|
|
164
|
+
// requires the hole to be its parent's only child). The title itself
|
|
165
|
+
// is edited in source mode; contentWithoutTitle() reads content from
|
|
166
|
+
// .carve-div-body so this shape re-parses without duplication.
|
|
167
|
+
return ['div', attrs,
|
|
168
|
+
['p', { class: 'admonition-title', contenteditable: 'false' }, title],
|
|
169
|
+
['div', { class: 'carve-div-body' }, 0],
|
|
170
|
+
];
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
addCommands() {
|
|
174
|
+
return {
|
|
175
|
+
setCarveDiv: (attributes) => ({ commands }) => {
|
|
176
|
+
return commands.wrapIn(this.name, attributes);
|
|
177
|
+
},
|
|
178
|
+
toggleCarveDiv: (attributes) => ({ commands }) => {
|
|
179
|
+
return commands.toggleWrap(this.name, attributes);
|
|
180
|
+
},
|
|
181
|
+
unsetCarveDiv: () => ({ commands }) => {
|
|
182
|
+
return commands.lift(this.name);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
export default CarveDiv;
|