@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,507 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
3
|
+
import CodeBlock from '@tiptap/extension-code-block';
|
|
4
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
5
|
+
import Subscript from '@tiptap/extension-subscript';
|
|
6
|
+
import Superscript from '@tiptap/extension-superscript';
|
|
7
|
+
import Underline from '@tiptap/extension-underline';
|
|
8
|
+
import Link from '@tiptap/extension-link';
|
|
9
|
+
import Image from '@tiptap/extension-image';
|
|
10
|
+
import Table from '@tiptap/extension-table';
|
|
11
|
+
import TableRow from '@tiptap/extension-table-row';
|
|
12
|
+
import TableCell from '@tiptap/extension-table-cell';
|
|
13
|
+
import TableHeader from '@tiptap/extension-table-header';
|
|
14
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
15
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
16
|
+
import BulletList from '@tiptap/extension-bullet-list';
|
|
17
|
+
import ListItem from '@tiptap/extension-list-item';
|
|
18
|
+
import HardBreak from '@tiptap/extension-hard-break';
|
|
19
|
+
|
|
20
|
+
import { CarveInsert } from './extensions/carve-insert.js';
|
|
21
|
+
import { CarveDelete } from './extensions/carve-delete.js';
|
|
22
|
+
import { CarveDiv } from './extensions/carve-div.js';
|
|
23
|
+
import { CarveTabSet, CarveTab } from './extensions/carve-tabs.js';
|
|
24
|
+
import { CarveSpan } from './extensions/carve-span.js';
|
|
25
|
+
import { CarveFootnote } from './extensions/carve-footnote.js';
|
|
26
|
+
import { CarveMath } from './extensions/carve-math.js';
|
|
27
|
+
import { CarveFootnoteDefinition } from './extensions/carve-footnote-definition.js';
|
|
28
|
+
import { CarveEmbed } from './extensions/carve-embed.js';
|
|
29
|
+
import { CarveAbbreviation } from './extensions/carve-abbreviation.js';
|
|
30
|
+
import { CarveDefinitionList, CarveDefinitionTerm, CarveDefinitionDescription } from './extensions/carve-definition-list.js';
|
|
31
|
+
import { CarveKeymap } from './extensions/carve-keymap.js';
|
|
32
|
+
import { CarveMention, CarveTag } from './extensions/carve-mention.js';
|
|
33
|
+
|
|
34
|
+
// Languages offered by the code-block picker. The current language is always
|
|
35
|
+
// shown even if it is not in this list.
|
|
36
|
+
const CODE_LANGS = [
|
|
37
|
+
{ value: '', label: 'Plain text' },
|
|
38
|
+
{ value: 'php', label: 'PHP' },
|
|
39
|
+
{ value: 'javascript', label: 'JavaScript' },
|
|
40
|
+
{ value: 'typescript', label: 'TypeScript' },
|
|
41
|
+
{ value: 'html', label: 'HTML' },
|
|
42
|
+
{ value: 'css', label: 'CSS' },
|
|
43
|
+
{ value: 'json', label: 'JSON' },
|
|
44
|
+
{ value: 'bash', label: 'Bash' },
|
|
45
|
+
{ value: 'python', label: 'Python' },
|
|
46
|
+
{ value: 'sql', label: 'SQL' },
|
|
47
|
+
{ value: 'yaml', label: 'YAML' },
|
|
48
|
+
{ value: 'markdown', label: 'Markdown' },
|
|
49
|
+
{ value: 'rust', label: 'Rust' },
|
|
50
|
+
{ value: 'go', label: 'Go' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* CarveKit - A Tiptap extension bundle for Carve markup
|
|
55
|
+
*
|
|
56
|
+
* Includes all standard Tiptap extensions plus Carve-specific marks:
|
|
57
|
+
* - CarveInsert: {+text+}
|
|
58
|
+
* - CarveDelete: {-text-}
|
|
59
|
+
* - CarveDiv: ::: containers
|
|
60
|
+
* - CarveSpan: [text]{.class}
|
|
61
|
+
* - CarveFootnote: [^label]
|
|
62
|
+
* - CarveEmbed: video/iframe embeds
|
|
63
|
+
* - CarveAbbreviation: [ABBR]{abbr="expansion"}
|
|
64
|
+
* - CarveDefinitionList: : term with definition
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```js
|
|
68
|
+
* import { Editor } from '@tiptap/core'
|
|
69
|
+
* import { CarveKit, serializeToCarve } from 'carve-grammars/tiptap'
|
|
70
|
+
*
|
|
71
|
+
* const editor = new Editor({
|
|
72
|
+
* element: document.getElementById('editor'),
|
|
73
|
+
* extensions: [CarveKit],
|
|
74
|
+
* onUpdate: ({ editor }) => {
|
|
75
|
+
* const carve = serializeToCarve(editor.getJSON())
|
|
76
|
+
* console.log(carve)
|
|
77
|
+
* },
|
|
78
|
+
* })
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example Configuration
|
|
82
|
+
* ```js
|
|
83
|
+
* import { CarveKit } from 'carve-grammars/tiptap'
|
|
84
|
+
*
|
|
85
|
+
* // Disable specific features
|
|
86
|
+
* CarveKit.configure({
|
|
87
|
+
* table: false,
|
|
88
|
+
* taskList: false,
|
|
89
|
+
* })
|
|
90
|
+
*
|
|
91
|
+
* // Configure specific extensions
|
|
92
|
+
* CarveKit.configure({
|
|
93
|
+
* link: {
|
|
94
|
+
* openOnClick: false,
|
|
95
|
+
* },
|
|
96
|
+
* codeBlock: {
|
|
97
|
+
* HTMLAttributes: {
|
|
98
|
+
* spellcheck: 'false',
|
|
99
|
+
* },
|
|
100
|
+
* },
|
|
101
|
+
* })
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export const CarveKit = Extension.create({
|
|
105
|
+
name: 'carveKit',
|
|
106
|
+
|
|
107
|
+
addExtensions() {
|
|
108
|
+
const extensions = [];
|
|
109
|
+
|
|
110
|
+
// StarterKit provides: Document, Paragraph, Text, Bold, Italic, Code,
|
|
111
|
+
// CodeBlock, Blockquote, BulletList, OrderedList, ListItem, Heading,
|
|
112
|
+
// HardBreak, HorizontalRule, Dropcursor, Gapcursor, History
|
|
113
|
+
if (this.options.starterKit !== false) {
|
|
114
|
+
extensions.push(StarterKit.configure({
|
|
115
|
+
// Disable CodeBlock from StarterKit, we add a custom one below
|
|
116
|
+
codeBlock: false,
|
|
117
|
+
// Disable default lists - we add custom ones that handle task-list
|
|
118
|
+
bulletList: false,
|
|
119
|
+
listItem: false,
|
|
120
|
+
// Disable HardBreak, we add a custom one with visible indicator
|
|
121
|
+
hardBreak: false,
|
|
122
|
+
...this.options.starterKit,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Custom HardBreak with visible indicator (shows ↵ symbol)
|
|
127
|
+
if (this.options.hardBreak !== false) {
|
|
128
|
+
const CustomHardBreak = HardBreak.extend({
|
|
129
|
+
addNodeView() {
|
|
130
|
+
return () => {
|
|
131
|
+
const dom = document.createElement('span');
|
|
132
|
+
dom.innerHTML = '<span class="hard-break">↵</span><br>';
|
|
133
|
+
return { dom };
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
extensions.push(CustomHardBreak.configure(this.options.hardBreak ?? {}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Custom CodeBlock that preserves data-language-raw for syntax highlighter options
|
|
141
|
+
if (this.options.codeBlock !== false) {
|
|
142
|
+
const CustomCodeBlock = CodeBlock.extend({
|
|
143
|
+
addAttributes() {
|
|
144
|
+
return {
|
|
145
|
+
...this.parent?.(),
|
|
146
|
+
languageRaw: {
|
|
147
|
+
default: null,
|
|
148
|
+
parseHTML: element => {
|
|
149
|
+
// Check parent <pre> for data-language-raw
|
|
150
|
+
const pre = element.closest('pre');
|
|
151
|
+
return pre?.getAttribute('data-language-raw') || null;
|
|
152
|
+
},
|
|
153
|
+
renderHTML: attributes => {
|
|
154
|
+
if (!attributes.languageRaw) return {};
|
|
155
|
+
return { 'data-language-raw': attributes.languageRaw };
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// Floating language picker: a <select> in the corner of every
|
|
162
|
+
// code block that shows the current language and edits it in
|
|
163
|
+
// place (the toolbar can't show a per-block value). Disable with
|
|
164
|
+
// CarveKit.configure({ codeBlock: { languagePicker: false } }).
|
|
165
|
+
addNodeView() {
|
|
166
|
+
if (this.options.languagePicker === false) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return ({ node, editor, getPos }) => {
|
|
170
|
+
let current = node;
|
|
171
|
+
const pre = document.createElement('pre');
|
|
172
|
+
if (node.attrs.languageRaw) {
|
|
173
|
+
pre.setAttribute('data-language-raw', node.attrs.languageRaw);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const select = document.createElement('select');
|
|
177
|
+
select.className = 'carve-code-lang';
|
|
178
|
+
select.contentEditable = 'false';
|
|
179
|
+
select.setAttribute('aria-label', 'Code language');
|
|
180
|
+
const fill = (lang) => {
|
|
181
|
+
select.innerHTML = '';
|
|
182
|
+
const opts = CODE_LANGS.slice();
|
|
183
|
+
if (lang && !opts.some(o => o.value === lang)) {
|
|
184
|
+
opts.push({ value: lang, label: lang });
|
|
185
|
+
}
|
|
186
|
+
for (const o of opts) {
|
|
187
|
+
const el = document.createElement('option');
|
|
188
|
+
el.value = o.value;
|
|
189
|
+
el.textContent = o.label;
|
|
190
|
+
if ((lang || '') === o.value) {
|
|
191
|
+
el.selected = true;
|
|
192
|
+
}
|
|
193
|
+
select.appendChild(el);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
fill(node.attrs.language || '');
|
|
197
|
+
// Keep clicks/keys inside the select from reaching PM.
|
|
198
|
+
select.addEventListener('mousedown', e => e.stopPropagation());
|
|
199
|
+
select.addEventListener('change', () => {
|
|
200
|
+
if (typeof getPos !== 'function') {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
editor.chain().focus().command(({ tr }) => {
|
|
204
|
+
tr.setNodeMarkup(getPos(), undefined, {
|
|
205
|
+
...current.attrs,
|
|
206
|
+
language: select.value || null,
|
|
207
|
+
});
|
|
208
|
+
return true;
|
|
209
|
+
}).run();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const code = document.createElement('code');
|
|
213
|
+
const applyLangClass = (lang) => {
|
|
214
|
+
code.className = lang ? `language-${lang}` : '';
|
|
215
|
+
};
|
|
216
|
+
applyLangClass(node.attrs.language || '');
|
|
217
|
+
pre.appendChild(select);
|
|
218
|
+
pre.appendChild(code);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
dom: pre,
|
|
222
|
+
contentDOM: code,
|
|
223
|
+
update: (updated) => {
|
|
224
|
+
if (updated.type !== current.type) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
current = updated;
|
|
228
|
+
if (select.value !== (updated.attrs.language || '')) {
|
|
229
|
+
fill(updated.attrs.language || '');
|
|
230
|
+
}
|
|
231
|
+
applyLangClass(updated.attrs.language || '');
|
|
232
|
+
return true;
|
|
233
|
+
},
|
|
234
|
+
// The <select> is chrome, not editable content.
|
|
235
|
+
ignoreMutation: (m) => select.contains(m.target),
|
|
236
|
+
stopEvent: (e) => select.contains(e.target),
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
extensions.push(CustomCodeBlock.configure({
|
|
242
|
+
HTMLAttributes: {
|
|
243
|
+
spellcheck: 'false',
|
|
244
|
+
},
|
|
245
|
+
...this.options.codeBlock,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Custom BulletList that excludes task-list class
|
|
250
|
+
if (this.options.bulletList !== false) {
|
|
251
|
+
const CustomBulletList = BulletList.extend({
|
|
252
|
+
parseHTML() {
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
tag: 'ul',
|
|
256
|
+
getAttrs: element => {
|
|
257
|
+
// Don't match task lists - let TaskList handle them.
|
|
258
|
+
// carve-php renders them as a plain <ul> whose items
|
|
259
|
+
// carry a checkbox (no .task-list class), so detect
|
|
260
|
+
// that shape too.
|
|
261
|
+
if (element.classList.contains('task-list')) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const hasCheckbox = Array.from(element.children).some(
|
|
265
|
+
(li) => li.tagName === 'LI' && li.querySelector('input[type="checkbox"]'),
|
|
266
|
+
);
|
|
267
|
+
if (hasCheckbox) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
return {};
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
extensions.push(CustomBulletList.configure(this.options.bulletList ?? {}));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Custom ListItem that excludes task items (those with checkboxes)
|
|
280
|
+
if (this.options.listItem !== false) {
|
|
281
|
+
const CustomListItem = ListItem.extend({
|
|
282
|
+
parseHTML() {
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
tag: 'li',
|
|
286
|
+
getAttrs: element => {
|
|
287
|
+
// Don't match list items with checkboxes - let TaskItem handle those
|
|
288
|
+
const checkbox = element.querySelector('input[type="checkbox"]');
|
|
289
|
+
if (checkbox) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
return {};
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
extensions.push(CustomListItem.configure(this.options.listItem ?? {}));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Highlight mark (built-in, maps to ==text==)
|
|
302
|
+
if (this.options.highlight !== false) {
|
|
303
|
+
extensions.push(Highlight.configure(this.options.highlight ?? {}));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Subscript mark (maps to the braced {,text,})
|
|
307
|
+
if (this.options.subscript !== false) {
|
|
308
|
+
extensions.push(Subscript.configure(this.options.subscript ?? {}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Superscript mark (maps to the braced {^text^})
|
|
312
|
+
if (this.options.superscript !== false) {
|
|
313
|
+
extensions.push(Superscript.configure(this.options.superscript ?? {}));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Underline mark (maps to _text_)
|
|
317
|
+
if (this.options.underline !== false) {
|
|
318
|
+
extensions.push(Underline.configure(this.options.underline ?? {}));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Link extension with keyboard shortcut
|
|
322
|
+
if (this.options.link !== false) {
|
|
323
|
+
extensions.push(
|
|
324
|
+
Link.configure({
|
|
325
|
+
openOnClick: false,
|
|
326
|
+
...this.options.link,
|
|
327
|
+
}).extend({
|
|
328
|
+
addKeyboardShortcuts() {
|
|
329
|
+
return {
|
|
330
|
+
'Mod-Shift-k': () => {
|
|
331
|
+
if (this.editor.isActive('link')) {
|
|
332
|
+
return this.editor.chain().focus().unsetLink().run();
|
|
333
|
+
}
|
|
334
|
+
const url = prompt('Enter URL:');
|
|
335
|
+
if (url) {
|
|
336
|
+
return this.editor.chain().focus().setLink({ href: url }).run();
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Image extension. Inline by default so `text  more` stays one
|
|
347
|
+
// paragraph (Carve images are inline); a block-level image just becomes a
|
|
348
|
+
// paragraph containing the inline image.
|
|
349
|
+
if (this.options.image !== false) {
|
|
350
|
+
extensions.push(Image.configure({ inline: true, ...(this.options.image ?? {}) }));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Table extensions
|
|
354
|
+
if (this.options.table !== false) {
|
|
355
|
+
extensions.push(Table.configure({
|
|
356
|
+
resizable: true,
|
|
357
|
+
...this.options.table,
|
|
358
|
+
}));
|
|
359
|
+
extensions.push(TableRow.configure(this.options.tableRow ?? {}));
|
|
360
|
+
extensions.push(TableCell.configure(this.options.tableCell ?? {}));
|
|
361
|
+
extensions.push(TableHeader.configure(this.options.tableHeader ?? {}));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Task list extensions - extend to match PHP output format
|
|
365
|
+
if (this.options.taskList !== false) {
|
|
366
|
+
// Extend TaskList to also match ul.task-list with high priority
|
|
367
|
+
const CustomTaskList = TaskList.extend({
|
|
368
|
+
parseHTML() {
|
|
369
|
+
return [
|
|
370
|
+
{ tag: 'ul[data-type="taskList"]', priority: 60 },
|
|
371
|
+
{ tag: 'ul.task-list', priority: 60 },
|
|
372
|
+
// carve-php: a plain <ul> whose items carry a checkbox.
|
|
373
|
+
{
|
|
374
|
+
tag: 'ul',
|
|
375
|
+
priority: 55,
|
|
376
|
+
getAttrs: (element) => Array.from(element.children).some(
|
|
377
|
+
(li) => li.tagName === 'LI' && li.querySelector('input[type="checkbox"]'),
|
|
378
|
+
) ? {} : false,
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
extensions.push(CustomTaskList.configure(this.options.taskList ?? {}));
|
|
384
|
+
|
|
385
|
+
// Extend TaskItem to also match li with checkbox input with high priority
|
|
386
|
+
const CustomTaskItem = TaskItem.extend({
|
|
387
|
+
addAttributes() {
|
|
388
|
+
return {
|
|
389
|
+
...this.parent?.(),
|
|
390
|
+
checked: {
|
|
391
|
+
default: false,
|
|
392
|
+
keepOnSplit: false,
|
|
393
|
+
parseHTML: element => {
|
|
394
|
+
// First check data-checked attribute
|
|
395
|
+
const dataChecked = element.getAttribute('data-checked');
|
|
396
|
+
if (dataChecked !== null) {
|
|
397
|
+
return dataChecked === 'true';
|
|
398
|
+
}
|
|
399
|
+
// Then check for checkbox input
|
|
400
|
+
const checkbox = element.querySelector('input[type="checkbox"]');
|
|
401
|
+
return checkbox?.hasAttribute('checked') || false;
|
|
402
|
+
},
|
|
403
|
+
renderHTML: attributes => ({
|
|
404
|
+
'data-checked': attributes.checked,
|
|
405
|
+
}),
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
},
|
|
409
|
+
parseHTML() {
|
|
410
|
+
return [
|
|
411
|
+
{ tag: 'li[data-type="taskItem"]', priority: 60 },
|
|
412
|
+
// Match list items that contain a checkbox input
|
|
413
|
+
{
|
|
414
|
+
tag: 'li',
|
|
415
|
+
priority: 60,
|
|
416
|
+
getAttrs: element => {
|
|
417
|
+
const checkbox = element.querySelector('input[type="checkbox"]');
|
|
418
|
+
if (checkbox) return {};
|
|
419
|
+
return false;
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
extensions.push(CustomTaskItem.configure({
|
|
426
|
+
nested: true,
|
|
427
|
+
...this.options.taskItem,
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Carve-specific extensions
|
|
432
|
+
if (this.options.carveInsert !== false) {
|
|
433
|
+
extensions.push(CarveInsert.configure(this.options.carveInsert ?? {}));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (this.options.carveDelete !== false) {
|
|
437
|
+
extensions.push(CarveDelete.configure(this.options.carveDelete ?? {}));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (this.options.carveDiv !== false) {
|
|
441
|
+
extensions.push(CarveDiv.configure(this.options.carveDiv ?? {}));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Tab sets (:::: tabs / ::: tab). Registered after CarveDiv but their
|
|
445
|
+
// div.tabs / div.tab parse rules use a higher priority, so a tab set is
|
|
446
|
+
// claimed here instead of by CarveDiv's generic div[class] rule.
|
|
447
|
+
if (this.options.carveTabs !== false) {
|
|
448
|
+
extensions.push(CarveTabSet.configure(this.options.carveTabs ?? {}));
|
|
449
|
+
extensions.push(CarveTab.configure(this.options.carveTabs ?? {}));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Span with class mark (maps to [text]{.class})
|
|
453
|
+
if (this.options.carveSpan !== false) {
|
|
454
|
+
extensions.push(CarveSpan.configure(this.options.carveSpan ?? {}));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Footnote reference node (maps to [^label])
|
|
458
|
+
if (this.options.carveFootnote !== false) {
|
|
459
|
+
extensions.push(CarveFootnote.configure(this.options.carveFootnote ?? {}));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Math node (maps to $`x`$ inline, $$`x`$$ display)
|
|
463
|
+
if (this.options.carveMath !== false) {
|
|
464
|
+
extensions.push(CarveMath.configure(this.options.carveMath ?? {}));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Footnote definition block (maps to [^label]: body)
|
|
468
|
+
if (this.options.carveFootnoteDefinition !== false) {
|
|
469
|
+
extensions.push(CarveFootnoteDefinition.configure(this.options.carveFootnoteDefinition ?? {}));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Embed node (preserves videos, oEmbed content)
|
|
473
|
+
if (this.options.carveEmbed !== false) {
|
|
474
|
+
extensions.push(CarveEmbed.configure(this.options.carveEmbed ?? {}));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Abbreviation mark (maps to [ABBR]{abbr="expansion"})
|
|
478
|
+
if (this.options.carveAbbreviation !== false) {
|
|
479
|
+
extensions.push(CarveAbbreviation.configure(this.options.carveAbbreviation ?? {}));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Definition list nodes (maps to : term with definition)
|
|
483
|
+
if (this.options.definitionList !== false) {
|
|
484
|
+
extensions.push(CarveDefinitionList.configure(this.options.definitionList ?? {}));
|
|
485
|
+
extensions.push(CarveDefinitionTerm.configure(this.options.definitionTerm ?? {}));
|
|
486
|
+
extensions.push(CarveDefinitionDescription.configure(this.options.definitionDescription ?? {}));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Mentions (@name) and tags (#tag); citations [@key] use a mention.
|
|
490
|
+
if (this.options.mention !== false) {
|
|
491
|
+
extensions.push(CarveMention.configure(this.options.mention ?? {}));
|
|
492
|
+
}
|
|
493
|
+
if (this.options.tag !== false) {
|
|
494
|
+
extensions.push(CarveTag.configure(this.options.tag ?? {}));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Keyboard shortcuts (Ctrl/Cmd+1..6 headings, clear formatting, Enter
|
|
498
|
+
// reset, ...). Opt out with CarveKit.configure({ keymap: false }).
|
|
499
|
+
if (this.options.keymap !== false) {
|
|
500
|
+
extensions.push(CarveKeymap.configure(this.options.keymap ?? {}));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return extensions;
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
export default CarveKit;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Mark, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carve Abbreviation extension for Tiptap
|
|
5
|
+
*
|
|
6
|
+
* Renders inline abbreviations with the `<abbr>` tag in the editor.
|
|
7
|
+
* Serializes to Carve as: [ABBR]{abbr="Full Text"}
|
|
8
|
+
*
|
|
9
|
+
* This is carve/djot-php's SemanticSpanExtension syntax: with that extension
|
|
10
|
+
* enabled, `[ABBR]{abbr="…"}` renders a real `<abbr title="…">` (which this
|
|
11
|
+
* mark's parseHTML reads back); without it, carve renders a `<span abbr="…">`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```js
|
|
15
|
+
* // In editor
|
|
16
|
+
* <abbr title="HyperText Markup Language">HTML</abbr>
|
|
17
|
+
*
|
|
18
|
+
* // Carve output
|
|
19
|
+
* [HTML]{abbr="HyperText Markup Language"}
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const CarveAbbreviation = Mark.create({
|
|
23
|
+
name: 'carveAbbreviation',
|
|
24
|
+
|
|
25
|
+
addAttributes() {
|
|
26
|
+
return {
|
|
27
|
+
title: {
|
|
28
|
+
default: null,
|
|
29
|
+
parseHTML: element => element.getAttribute('title'),
|
|
30
|
+
renderHTML: attributes => {
|
|
31
|
+
if (!attributes.title) return {};
|
|
32
|
+
return { title: attributes.title };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
parseHTML() {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
tag: 'abbr[title]',
|
|
42
|
+
priority: 51,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
renderHTML({ HTMLAttributes }) {
|
|
48
|
+
return ['abbr', mergeAttributes(HTMLAttributes), 0];
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
addCommands() {
|
|
52
|
+
return {
|
|
53
|
+
setAbbreviation: attributes => ({ commands }) => {
|
|
54
|
+
return commands.setMark(this.name, attributes);
|
|
55
|
+
},
|
|
56
|
+
toggleAbbreviation: attributes => ({ commands }) => {
|
|
57
|
+
return commands.toggleMark(this.name, attributes);
|
|
58
|
+
},
|
|
59
|
+
unsetAbbreviation: () => ({ commands }) => {
|
|
60
|
+
return commands.unsetMark(this.name);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
addKeyboardShortcuts() {
|
|
66
|
+
return {
|
|
67
|
+
// Auto-exit abbreviation mark when pressing space
|
|
68
|
+
'Space': () => {
|
|
69
|
+
if (this.editor.isActive(this.name)) {
|
|
70
|
+
this.editor.commands.unsetMark(this.name);
|
|
71
|
+
return false; // Let space be typed normally
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export default CarveAbbreviation;
|