@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,101 @@
|
|
|
1
|
+
import { Mark, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carve Span mark extension for Tiptap
|
|
5
|
+
*
|
|
6
|
+
* Renders as [text]{.class} in Carve markup
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* import { CarveSpan } from 'carve-grammars/tiptap'
|
|
11
|
+
*
|
|
12
|
+
* const editor = new Editor({
|
|
13
|
+
* extensions: [CarveSpan],
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* // Apply span with class
|
|
17
|
+
* editor.chain().focus().setCarveSpan({ class: 'highlight' }).run()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const CarveSpan = Mark.create({
|
|
21
|
+
name: 'carveSpan',
|
|
22
|
+
|
|
23
|
+
addAttributes() {
|
|
24
|
+
return {
|
|
25
|
+
class: {
|
|
26
|
+
default: 'custom',
|
|
27
|
+
parseHTML: element => {
|
|
28
|
+
// First check data-carve-class, then fall back to class attribute
|
|
29
|
+
const carveClass = element.getAttribute('data-carve-class');
|
|
30
|
+
if (carveClass) return carveClass;
|
|
31
|
+
// Extract class from className, filtering out carve-span
|
|
32
|
+
const className = element.className || '';
|
|
33
|
+
return className.replace('carve-span', '').trim() || 'custom';
|
|
34
|
+
},
|
|
35
|
+
renderHTML: attributes => {
|
|
36
|
+
return { 'data-carve-class': attributes.class };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
id: {
|
|
40
|
+
default: null,
|
|
41
|
+
parseHTML: element => element.getAttribute('id') || null,
|
|
42
|
+
renderHTML: attributes => {
|
|
43
|
+
if (!attributes.id) return {};
|
|
44
|
+
return { id: attributes.id };
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
parseHTML() {
|
|
51
|
+
return [
|
|
52
|
+
{ tag: 'span[data-carve-class]' },
|
|
53
|
+
// Also match spans with class attributes from PHP renderer
|
|
54
|
+
{
|
|
55
|
+
tag: 'span[class]',
|
|
56
|
+
getAttrs: element => {
|
|
57
|
+
// Skip spans that are part of code highlighting or other editor elements
|
|
58
|
+
const className = element.className || '';
|
|
59
|
+
// Skip token spans (Phiki/Torchlight syntax highlighting)
|
|
60
|
+
if (className.includes('token') || className.includes('phiki') ||
|
|
61
|
+
className.includes('torchlight') || className.includes('ProseMirror')) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// Skip if inside a pre or code element
|
|
65
|
+
if (element.closest('pre') || element.closest('code')) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
// Match spans with simple classes (likely from Carve [text]{.class})
|
|
69
|
+
if (/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(className)) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
renderHTML({ HTMLAttributes }) {
|
|
79
|
+
const className = HTMLAttributes['data-carve-class'] || 'custom';
|
|
80
|
+
return ['span', mergeAttributes(HTMLAttributes, {
|
|
81
|
+
class: `carve-span ${className}`,
|
|
82
|
+
'data-carve-class': className,
|
|
83
|
+
}), 0];
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
addCommands() {
|
|
87
|
+
return {
|
|
88
|
+
setCarveSpan: (attributes) => ({ commands }) => {
|
|
89
|
+
return commands.setMark(this.name, attributes);
|
|
90
|
+
},
|
|
91
|
+
toggleCarveSpan: (attributes) => ({ commands }) => {
|
|
92
|
+
return commands.toggleMark(this.name, attributes);
|
|
93
|
+
},
|
|
94
|
+
unsetCarveSpan: () => ({ commands }) => {
|
|
95
|
+
return commands.unsetMark(this.name);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export default CarveSpan;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carve tab-set nodes for Tiptap.
|
|
5
|
+
*
|
|
6
|
+
* A `:::: tabs` container holds `::: tab` children, each carrying a `label`
|
|
7
|
+
* and an optional `selected` flag. In the editor seed (rendered without the
|
|
8
|
+
* TabsExtension) this is plain HTML - `<div class="tabs"><div class="tab"
|
|
9
|
+
* label="First" selected>…</div></div>` - so these nodes parse that raw form,
|
|
10
|
+
* capturing the label/selected attributes the generic CarveDiv would otherwise
|
|
11
|
+
* drop. They round-trip back to Carve via serializer.js.
|
|
12
|
+
*
|
|
13
|
+
* Two nodes:
|
|
14
|
+
* - `carveTabSet` - the `<div class="tabs">` wrapper, holding `carveTab+`.
|
|
15
|
+
* - `carveTab` - one `<div class="tab">` panel, attrs `{ label, selected }`.
|
|
16
|
+
*
|
|
17
|
+
* The parse rules use a high priority so a `div.tabs` / `div.tab` is claimed
|
|
18
|
+
* here rather than by CarveDiv's generic `div[class]` rule.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const CarveTabSet = Node.create({
|
|
22
|
+
name: 'carveTabSet',
|
|
23
|
+
|
|
24
|
+
group: 'block',
|
|
25
|
+
|
|
26
|
+
content: 'carveTab+',
|
|
27
|
+
|
|
28
|
+
defining: true,
|
|
29
|
+
|
|
30
|
+
parseHTML() {
|
|
31
|
+
return [{ tag: 'div.tabs', priority: 60 }];
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
renderHTML({ HTMLAttributes }) {
|
|
35
|
+
return ['div', mergeAttributes(HTMLAttributes, { class: 'tabs' }), 0];
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Interactive tab bar. The wrapper holds a (non-editable) row of buttons -
|
|
39
|
+
// one per tab, labelled from each carveTab's `label` - plus the editable
|
|
40
|
+
// panel container (contentDOM) where the tab nodes render. Active-tab state
|
|
41
|
+
// is view-local: a `data-active` index on the wrapper that CSS uses to show
|
|
42
|
+
// one panel at a time; clicking a button just moves it. No ProseMirror
|
|
43
|
+
// transaction, so switching tabs never touches the document or serialize.
|
|
44
|
+
addNodeView() {
|
|
45
|
+
return ({ node }) => {
|
|
46
|
+
const dom = document.createElement('div');
|
|
47
|
+
dom.className = 'carve-tabset';
|
|
48
|
+
dom.setAttribute('data-active', '0');
|
|
49
|
+
|
|
50
|
+
const bar = document.createElement('div');
|
|
51
|
+
bar.className = 'carve-tabset-bar';
|
|
52
|
+
bar.contentEditable = 'false';
|
|
53
|
+
|
|
54
|
+
const panels = document.createElement('div');
|
|
55
|
+
panels.className = 'carve-tabset-panels';
|
|
56
|
+
|
|
57
|
+
const renderBar = (n) => {
|
|
58
|
+
bar.textContent = '';
|
|
59
|
+
n.forEach((tab, _offset, index) => {
|
|
60
|
+
const btn = document.createElement('button');
|
|
61
|
+
btn.type = 'button';
|
|
62
|
+
btn.className = 'carve-tabset-tab';
|
|
63
|
+
btn.textContent = tab.attrs?.label || `Tab ${index + 1}`;
|
|
64
|
+
btn.addEventListener('mousedown', (e) => {
|
|
65
|
+
// mousedown (not click) so the editor selection isn't
|
|
66
|
+
// moved into the button before we switch.
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
dom.setAttribute('data-active', String(index));
|
|
69
|
+
});
|
|
70
|
+
bar.appendChild(btn);
|
|
71
|
+
});
|
|
72
|
+
// Default the active tab to the one flagged `selected`, else 0.
|
|
73
|
+
let active = 0;
|
|
74
|
+
n.forEach((tab, _o, i) => {
|
|
75
|
+
if (tab.attrs?.selected) active = i;
|
|
76
|
+
});
|
|
77
|
+
dom.setAttribute('data-active', String(active));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
renderBar(node);
|
|
81
|
+
dom.appendChild(bar);
|
|
82
|
+
dom.appendChild(panels);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
dom,
|
|
86
|
+
contentDOM: panels,
|
|
87
|
+
update: (updated) => {
|
|
88
|
+
if (updated.type.name !== 'carveTabSet') return false;
|
|
89
|
+
const active = dom.getAttribute('data-active');
|
|
90
|
+
renderBar(updated);
|
|
91
|
+
// renderBar resets active to the selected tab; keep the
|
|
92
|
+
// user's current choice if it is still in range.
|
|
93
|
+
if (active != null && Number(active) < updated.childCount) {
|
|
94
|
+
dom.setAttribute('data-active', active);
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const CarveTab = Node.create({
|
|
104
|
+
name: 'carveTab',
|
|
105
|
+
|
|
106
|
+
content: 'block+',
|
|
107
|
+
|
|
108
|
+
defining: true,
|
|
109
|
+
|
|
110
|
+
addAttributes() {
|
|
111
|
+
return {
|
|
112
|
+
label: {
|
|
113
|
+
default: null,
|
|
114
|
+
// Attribute form (`{label="..."}` -> label="...") or the
|
|
115
|
+
// canonical opener form (`::: tab [Label]`), which both engines
|
|
116
|
+
// render as a leading <p class="div-label"> child.
|
|
117
|
+
parseHTML: element => {
|
|
118
|
+
const attr = element.getAttribute('label');
|
|
119
|
+
if (attr != null) return attr;
|
|
120
|
+
const first = element.firstElementChild;
|
|
121
|
+
return first && first.tagName === 'P' && first.classList.contains('div-label')
|
|
122
|
+
? first.textContent
|
|
123
|
+
: null;
|
|
124
|
+
},
|
|
125
|
+
renderHTML: attributes => (attributes.label == null ? {} : { label: attributes.label }),
|
|
126
|
+
},
|
|
127
|
+
// `selected` is a boolean flag: present (any value, incl. "") means
|
|
128
|
+
// selected. Serialized as a bare `selected` in the attribute line.
|
|
129
|
+
selected: {
|
|
130
|
+
default: false,
|
|
131
|
+
parseHTML: element => element.hasAttribute('selected'),
|
|
132
|
+
renderHTML: attributes => (attributes.selected ? { selected: '' } : {}),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
parseHTML() {
|
|
138
|
+
return [{
|
|
139
|
+
tag: 'div.tab',
|
|
140
|
+
priority: 60,
|
|
141
|
+
// The div-label paragraph becomes the label attr above; keep it
|
|
142
|
+
// out of the editable panel content.
|
|
143
|
+
contentElement: element => {
|
|
144
|
+
const first = element.firstElementChild;
|
|
145
|
+
if (!first || first.tagName !== 'P' || !first.classList.contains('div-label')) {
|
|
146
|
+
return element;
|
|
147
|
+
}
|
|
148
|
+
const clone = element.cloneNode(true);
|
|
149
|
+
clone.removeChild(clone.firstElementChild);
|
|
150
|
+
return clone;
|
|
151
|
+
},
|
|
152
|
+
}];
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
renderHTML({ HTMLAttributes }) {
|
|
156
|
+
return ['div', mergeAttributes(HTMLAttributes, { class: 'tab' }), 0];
|
|
157
|
+
},
|
|
158
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carve-specific Tiptap extensions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { CarveInsert } from './carve-insert.js';
|
|
6
|
+
export { CarveDelete } from './carve-delete.js';
|
|
7
|
+
export { CarveDiv } from './carve-div.js';
|
|
8
|
+
export { CarveSpan } from './carve-span.js';
|
|
9
|
+
export { CarveFootnote } from './carve-footnote.js';
|
|
10
|
+
export { CarveFootnoteDefinition } from './carve-footnote-definition.js';
|
|
11
|
+
export { CarveMath } from './carve-math.js';
|
|
12
|
+
export { CarveEmbed } from './carve-embed.js';
|
|
13
|
+
export { CarveAbbreviation } from './carve-abbreviation.js';
|
|
14
|
+
export { CarveDefinitionList, CarveDefinitionTerm, CarveDefinitionDescription } from './carve-definition-list.js';
|
package/tiptap/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carve Grammars - Tiptap Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides Carve markup support for Tiptap editors.
|
|
5
|
+
*
|
|
6
|
+
* @example Basic usage with CarveKit
|
|
7
|
+
* ```js
|
|
8
|
+
* import { Editor } from '@tiptap/core'
|
|
9
|
+
* import { CarveKit, serializeToCarve } from 'carve-grammars/tiptap'
|
|
10
|
+
*
|
|
11
|
+
* const editor = new Editor({
|
|
12
|
+
* element: document.getElementById('editor'),
|
|
13
|
+
* extensions: [CarveKit],
|
|
14
|
+
* onUpdate: ({ editor }) => {
|
|
15
|
+
* const carve = serializeToCarve(editor.getJSON())
|
|
16
|
+
* console.log(carve)
|
|
17
|
+
* },
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example Using individual extensions
|
|
22
|
+
* ```js
|
|
23
|
+
* import { Editor } from '@tiptap/core'
|
|
24
|
+
* import StarterKit from '@tiptap/starter-kit'
|
|
25
|
+
* import { CarveInsert, CarveDelete, CarveDiv, serializeToCarve } from 'carve-grammars/tiptap'
|
|
26
|
+
*
|
|
27
|
+
* const editor = new Editor({
|
|
28
|
+
* extensions: [
|
|
29
|
+
* StarterKit,
|
|
30
|
+
* CarveInsert,
|
|
31
|
+
* CarveDelete,
|
|
32
|
+
* CarveDiv,
|
|
33
|
+
* ],
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @module carve-grammars/tiptap
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// Main kit
|
|
41
|
+
export { CarveKit } from './carve-kit.js';
|
|
42
|
+
|
|
43
|
+
// Individual extensions
|
|
44
|
+
export { CarveInsert } from './extensions/carve-insert.js';
|
|
45
|
+
export { CarveDelete } from './extensions/carve-delete.js';
|
|
46
|
+
export { CarveDiv } from './extensions/carve-div.js';
|
|
47
|
+
export { CarveMath } from './extensions/carve-math.js';
|
|
48
|
+
export { CarveFootnoteDefinition } from './extensions/carve-footnote-definition.js';
|
|
49
|
+
export { CarveKeymap } from './extensions/carve-keymap.js';
|
|
50
|
+
export { CarveMention, CarveTag } from './extensions/carve-mention.js';
|
|
51
|
+
|
|
52
|
+
// Serializer
|
|
53
|
+
export { serializeToCarve, escapeCarve, carveMediaDirective } from './serializer.js';
|