@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.
@@ -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';
@@ -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';