@0m0g1/griot 0.1.3 → 0.1.5
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/README.md +392 -127
- package/package.json +1 -1
- package/src/Griot.js +36 -35
- package/src/blocks/BlockRenderer.js +240 -93
- package/src/blocks/BlockSchema.js +32 -19
- package/src/editor/SlashMenu.js +197 -0
- package/src/viewer/GalleryRenderer.js +417 -0
- package/src/viewer/Lightbox.js +320 -0
- package/src/viewer/Viewer.js +47 -8
package/src/Griot.js
CHANGED
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
// ─── Griot.js ─────────────────────────────────────────────────────────────────
|
|
2
|
-
// Public facade. Import from here — never from internal modules directly.
|
|
2
|
+
// Public facade. Import from here only — never from internal modules directly.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// Schema
|
|
22
|
-
// getBlockDef, getAllTypes, defaultMeta, BlockSchema
|
|
4
|
+
// Classes Editor, Viewer, FormatToolbar, SlashMenu, DropHandler
|
|
5
|
+
// Document createDocument, createBlock, cloneBlock
|
|
6
|
+
// updateBlock, insertBlockAfter, insertBlockBefore,
|
|
7
|
+
// removeBlock, moveBlock, splitBlock, mergeBlockWithPrev,
|
|
8
|
+
// getBlock, getBlockIndex, getBlockBefore, getBlockAfter,
|
|
9
|
+
// toJSON, fromJSON
|
|
10
|
+
// Block anchorId, scrollToBlock, isTextBlock, isValidBlock
|
|
11
|
+
// TEXT_TYPES, ALL_TYPES
|
|
12
|
+
// Inline tokenizeInline, renderInlineToDOM, renderInlineToHTML,
|
|
13
|
+
// escHtml, escAttr, TOKEN
|
|
14
|
+
// Schema getBlockDef, getAllTypes, getTypesByCategory,
|
|
15
|
+
// defaultMeta, BlockSchema
|
|
16
|
+
// Keyboard attachKeyboardHandler, getCursorOffset, getSelectionOffsets,
|
|
17
|
+
// setCursorOffset, focusAtEnd, focusAtStart
|
|
18
|
+
// URL helpers resolveYouTube, resolveVimeo, resolveSpotify, resolveSoundCloud
|
|
19
|
+
// Gallery renderGallery, lightbox
|
|
23
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
21
|
|
|
25
|
-
// Core
|
|
26
22
|
export {
|
|
27
23
|
createBlock, cloneBlock, isTextBlock, isValidBlock,
|
|
28
|
-
anchorId, scrollToBlock,
|
|
29
|
-
TEXT_TYPES, ALL_TYPES,
|
|
24
|
+
anchorId, scrollToBlock, TEXT_TYPES, ALL_TYPES,
|
|
30
25
|
} from './core/Block.js';
|
|
31
26
|
|
|
32
27
|
export {
|
|
@@ -38,17 +33,23 @@ export {
|
|
|
38
33
|
|
|
39
34
|
export { History } from './core/History.js';
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
export {
|
|
44
|
-
renderInlineToDOM, renderInlineToHTML, escHtml, escAttr,
|
|
45
|
-
} from './inline/InlineRenderer.js';
|
|
36
|
+
export { tokenizeInline, TOKEN } from './inline/InlineLexer.js';
|
|
37
|
+
export { renderInlineToDOM, renderInlineToHTML, escHtml, escAttr } from './inline/InlineRenderer.js';
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
export {
|
|
49
|
-
export {
|
|
50
|
-
export {
|
|
39
|
+
export { getBlockDef, getAllTypes, getTypesByCategory, defaultMeta } from './blocks/BlockSchema.js';
|
|
40
|
+
export { default as BlockSchema } from './blocks/BlockSchema.js';
|
|
41
|
+
export { renderBlock, resolveYouTube, resolveVimeo, resolveSpotify, resolveSoundCloud } from './blocks/BlockRenderer.js';
|
|
42
|
+
export { renderGallery } from './blocks/GalleryRenderer.js';
|
|
43
|
+
export { lightbox } from './blocks/Lightbox.js';
|
|
44
|
+
|
|
45
|
+
export { Editor } from './editor/Editor.js';
|
|
46
|
+
export { FormatToolbar } from './editor/FormatToolbar.js';
|
|
47
|
+
export { SlashMenu } from './editor/SlashMenu.js';
|
|
48
|
+
export { DropHandler } from './editor/DropHandler.js';
|
|
49
|
+
export {
|
|
50
|
+
attachKeyboardHandler,
|
|
51
|
+
getCursorOffset, getSelectionOffsets, setCursorOffset,
|
|
52
|
+
focusAtEnd, focusAtStart,
|
|
53
|
+
} from './editor/Keyboard.js';
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
export { Editor } from './editor/Editor.js';
|
|
54
|
-
export { Viewer } from './viewer/Viewer.js';
|
|
55
|
+
export { Viewer } from './viewer/Viewer.js';
|
|
@@ -1,46 +1,48 @@
|
|
|
1
1
|
// ─── BlockRenderer.js ─────────────────────────────────────────────────────────
|
|
2
|
-
// Renders a single block
|
|
3
|
-
// Used by
|
|
2
|
+
// Renders a single block → DOM element.
|
|
3
|
+
// Used by Viewer (read-only) and Editor preview layer.
|
|
4
4
|
//
|
|
5
5
|
// Options:
|
|
6
|
-
// books
|
|
7
|
-
// onEventClick
|
|
8
|
-
// onCiteClick
|
|
9
|
-
// editable — if true, skips event listeners (Editor manages them)
|
|
6
|
+
// books — array of book objects
|
|
7
|
+
// onEventClick — (eventId) => void
|
|
8
|
+
// onCiteClick — (blockId) => void
|
|
10
9
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
10
|
|
|
12
|
-
import { anchorId }
|
|
11
|
+
import { anchorId } from '../core/Block.js';
|
|
13
12
|
import { renderInlineToDOM, renderInlineToHTML, escHtml, escAttr } from '../inline/InlineRenderer.js';
|
|
14
|
-
import { getBlockDef }
|
|
13
|
+
import { getBlockDef } from './BlockSchema.js';
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const LAYOUT_OPTIONS = ['grid','masonry','carousel','strip'];
|
|
16
|
+
|
|
17
|
+
// ─── Public ───────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function renderBlock(block, opts = {}) {
|
|
20
|
+
const el = _render(block, opts);
|
|
19
21
|
if (el) {
|
|
20
|
-
el.id
|
|
21
|
-
el.dataset.blockId
|
|
22
|
+
el.id = anchorId(block.id);
|
|
23
|
+
el.dataset.blockId = block.id;
|
|
22
24
|
el.dataset.blockType = block.type;
|
|
23
25
|
}
|
|
24
26
|
return el;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
// ───
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
onCiteClick: opts.onCiteClick,
|
|
32
|
-
});
|
|
29
|
+
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function il(text, opts) {
|
|
32
|
+
return renderInlineToDOM(text, { onEventClick: opts.onEventClick, onCiteClick: opts.onCiteClick });
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function _render(block, opts) {
|
|
36
|
-
const { text, meta = {}, type } = block;
|
|
36
|
+
const { text = '', meta = {}, type } = block;
|
|
37
37
|
|
|
38
38
|
switch (type) {
|
|
39
39
|
|
|
40
|
+
// ── Text ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
40
42
|
case 'paragraph': {
|
|
41
43
|
const el = document.createElement('p');
|
|
42
44
|
el.className = 'griot-block griot-paragraph';
|
|
43
|
-
if (text) el.appendChild(
|
|
45
|
+
if (text) el.appendChild(il(text, opts));
|
|
44
46
|
return el;
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -55,70 +57,185 @@ function _render(block, opts) {
|
|
|
55
57
|
case 'blockquote': {
|
|
56
58
|
const el = document.createElement('blockquote');
|
|
57
59
|
el.className = 'griot-block griot-blockquote';
|
|
58
|
-
if (text) el.appendChild(
|
|
60
|
+
if (text) el.appendChild(il(text, opts));
|
|
59
61
|
return el;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
case 'callout':
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
case 'callout':
|
|
65
|
+
case 'callout_warning':
|
|
66
|
+
case 'callout_tip':
|
|
67
|
+
case 'callout_danger': {
|
|
68
|
+
const ICONS = { callout:'💡', callout_warning:'⚠️', callout_tip:'✅', callout_danger:'🚨' };
|
|
69
|
+
const el = document.createElement('div');
|
|
70
|
+
const icon = document.createElement('span');
|
|
71
|
+
const body = document.createElement('div');
|
|
72
|
+
el.className = `griot-block griot-callout griot-callout--${type.replace('callout_','') || 'info'}`;
|
|
67
73
|
icon.className = 'griot-callout__icon';
|
|
68
74
|
body.className = 'griot-callout__body';
|
|
69
|
-
icon.textContent = meta.icon ?? '💡';
|
|
70
|
-
if (text) body.appendChild(
|
|
71
|
-
el.
|
|
72
|
-
el.appendChild(body);
|
|
75
|
+
icon.textContent = meta.icon ?? ICONS[type] ?? '💡';
|
|
76
|
+
if (text) body.appendChild(il(text, opts));
|
|
77
|
+
el.append(icon, body);
|
|
73
78
|
return el;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
case 'code': {
|
|
77
82
|
const pre = document.createElement('pre');
|
|
78
83
|
const code = document.createElement('code');
|
|
79
|
-
pre.className
|
|
80
|
-
if (meta.language) code.className = `language-${meta.language}`;
|
|
84
|
+
pre.className = 'griot-block griot-code';
|
|
85
|
+
if (meta.language) { pre.dataset.language = meta.language; code.className = `language-${meta.language}`; }
|
|
81
86
|
code.textContent = text ?? '';
|
|
82
87
|
pre.appendChild(code);
|
|
83
88
|
return pre;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
case '
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
case 'list_ul':
|
|
92
|
+
case 'list_ol': {
|
|
93
|
+
const tag = type === 'list_ul' ? 'ul' : 'ol';
|
|
94
|
+
const el = document.createElement(tag);
|
|
95
|
+
el.className = `griot-block griot-list griot-list--${tag}`;
|
|
96
|
+
for (const item of (text ?? '').split('\n').filter(l => l.trim())) {
|
|
97
|
+
const li = document.createElement('li');
|
|
98
|
+
li.appendChild(il(item, opts));
|
|
99
|
+
el.appendChild(li);
|
|
100
|
+
}
|
|
89
101
|
return el;
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
// ── Media ─────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
92
106
|
case 'image': {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const fig = document.createElement('figure');
|
|
108
|
+
fig.className = `griot-block griot-image griot-image--${meta.width ?? 'full'}`;
|
|
109
|
+
if (meta.uploading) {
|
|
110
|
+
const sp = document.createElement('div');
|
|
111
|
+
sp.className = 'griot-media-uploading';
|
|
112
|
+
sp.textContent = 'Uploading…';
|
|
113
|
+
fig.appendChild(sp);
|
|
114
|
+
} else if (meta.src) {
|
|
115
|
+
const img = document.createElement('img');
|
|
116
|
+
img.src = meta.src; img.alt = meta.alt ?? '';
|
|
117
|
+
fig.appendChild(img);
|
|
118
|
+
if (meta.caption) {
|
|
119
|
+
const cap = document.createElement('figcaption');
|
|
120
|
+
cap.textContent = meta.caption;
|
|
121
|
+
fig.appendChild(cap);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return fig;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case 'video': {
|
|
128
|
+
const fig = document.createElement('figure');
|
|
129
|
+
fig.className = 'griot-block griot-video';
|
|
130
|
+
const embed = meta.embedUrl ?? _ytEmbed(meta.src) ?? _vimeoEmbed(meta.src);
|
|
131
|
+
if (embed) {
|
|
132
|
+
const iframe = document.createElement('iframe');
|
|
133
|
+
iframe.src = embed; iframe.className = 'griot-video__iframe';
|
|
134
|
+
iframe.frameBorder = '0';
|
|
135
|
+
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
|
|
136
|
+
iframe.allowFullscreen = true;
|
|
137
|
+
fig.appendChild(iframe);
|
|
138
|
+
} else if (meta.src) {
|
|
139
|
+
const v = document.createElement('video');
|
|
140
|
+
v.src = meta.src; v.controls = true; v.className = 'griot-video__native';
|
|
141
|
+
fig.appendChild(v);
|
|
142
|
+
}
|
|
143
|
+
if (meta.caption) { const c = document.createElement('figcaption'); c.textContent = meta.caption; fig.appendChild(c); }
|
|
144
|
+
return fig;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case 'audio': {
|
|
148
|
+
const fig = document.createElement('figure');
|
|
149
|
+
fig.className = 'griot-block griot-audio';
|
|
150
|
+
const embed = meta.embedUrl ?? _spotifyEmbed(meta.src) ?? _scEmbed(meta.src);
|
|
151
|
+
if (embed) {
|
|
152
|
+
const iframe = document.createElement('iframe');
|
|
153
|
+
iframe.src = embed; iframe.className = 'griot-audio__iframe';
|
|
154
|
+
iframe.frameBorder = '0';
|
|
155
|
+
iframe.allow = 'autoplay; clipboard-write; encrypted-media; fullscreen';
|
|
156
|
+
fig.appendChild(iframe);
|
|
157
|
+
} else if (meta.src) {
|
|
158
|
+
const a = document.createElement('audio');
|
|
159
|
+
a.src = meta.src; a.controls = true; a.className = 'griot-audio__native';
|
|
160
|
+
fig.appendChild(a);
|
|
103
161
|
}
|
|
104
|
-
|
|
162
|
+
if (meta.caption) { const c = document.createElement('figcaption'); c.textContent = meta.caption; fig.appendChild(c); }
|
|
163
|
+
return fig;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'gallery': {
|
|
167
|
+
return _renderGallery(meta.items ?? [], meta.layout ?? 'grid', opts);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'embed': {
|
|
171
|
+
const fig = document.createElement('figure');
|
|
172
|
+
fig.className = 'griot-block griot-embed';
|
|
173
|
+
if (meta.src) {
|
|
174
|
+
const iframe = document.createElement('iframe');
|
|
175
|
+
iframe.src = meta.src;
|
|
176
|
+
iframe.style.height = `${meta.height ?? 400}px`;
|
|
177
|
+
iframe.className = 'griot-embed__iframe';
|
|
178
|
+
iframe.frameBorder = '0';
|
|
179
|
+
iframe.allow = 'autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media';
|
|
180
|
+
iframe.allowFullscreen = true;
|
|
181
|
+
fig.appendChild(iframe);
|
|
182
|
+
}
|
|
183
|
+
if (meta.caption) { const c = document.createElement('figcaption'); c.textContent = meta.caption; fig.appendChild(c); }
|
|
184
|
+
return fig;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Structure ─────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
case 'table': {
|
|
190
|
+
const headers = Array.isArray(meta.headers) ? meta.headers : [];
|
|
191
|
+
const rows = Array.isArray(meta.rows) ? meta.rows : [];
|
|
192
|
+
const colCount = Math.max(headers.length, ...rows.map(r => r.length), 1);
|
|
193
|
+
const wrap = document.createElement('div');
|
|
194
|
+
wrap.className = 'griot-block griot-table-wrap';
|
|
195
|
+
const table = document.createElement('table');
|
|
196
|
+
table.className = 'griot-table';
|
|
197
|
+
if (headers.length) {
|
|
198
|
+
const thead = document.createElement('thead');
|
|
199
|
+
const tr = document.createElement('tr');
|
|
200
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
201
|
+
const th = document.createElement('th');
|
|
202
|
+
th.appendChild(il(headers[ci] ?? '', opts));
|
|
203
|
+
tr.appendChild(th);
|
|
204
|
+
}
|
|
205
|
+
thead.appendChild(tr); table.appendChild(thead);
|
|
206
|
+
}
|
|
207
|
+
const tbody = document.createElement('tbody');
|
|
208
|
+
for (const row of rows) {
|
|
209
|
+
const tr = document.createElement('tr');
|
|
210
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
211
|
+
const td = document.createElement('td');
|
|
212
|
+
td.appendChild(il(row[ci] ?? '', opts));
|
|
213
|
+
tr.appendChild(td);
|
|
214
|
+
}
|
|
215
|
+
tbody.appendChild(tr);
|
|
216
|
+
}
|
|
217
|
+
table.appendChild(tbody); wrap.appendChild(table);
|
|
218
|
+
return wrap;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'divider': {
|
|
222
|
+
const el = document.createElement('hr');
|
|
223
|
+
el.className = 'griot-block griot-divider';
|
|
224
|
+
return el;
|
|
105
225
|
}
|
|
106
226
|
|
|
107
227
|
case 'timeline_ref': {
|
|
108
228
|
const el = document.createElement('div');
|
|
109
229
|
el.className = 'griot-block griot-timeline-ref';
|
|
110
230
|
if (meta.eventId && opts.onEventClick) {
|
|
111
|
-
el.setAttribute('role',
|
|
112
|
-
el.tabIndex = 0;
|
|
231
|
+
el.setAttribute('role','button'); el.tabIndex = 0;
|
|
113
232
|
el.addEventListener('click', () => opts.onEventClick(meta.eventId));
|
|
114
|
-
el.addEventListener('keydown', (e)
|
|
115
|
-
if (e.key === 'Enter' || e.key === ' ') opts.onEventClick(meta.eventId);
|
|
116
|
-
});
|
|
233
|
+
el.addEventListener('keydown', e => { if (e.key==='Enter'||e.key===' ') opts.onEventClick(meta.eventId); });
|
|
117
234
|
}
|
|
118
235
|
el.innerHTML = `
|
|
119
236
|
<span class="griot-timeline-ref__icon">⏱</span>
|
|
120
237
|
<div class="griot-timeline-ref__body">
|
|
121
|
-
<div class="griot-timeline-ref__title">${escHtml(meta.eventTitle
|
|
238
|
+
<div class="griot-timeline-ref__title">${escHtml(meta.eventTitle||'Timeline Event')}</div>
|
|
122
239
|
${meta.note ? `<div class="griot-timeline-ref__note">${escHtml(meta.note)}</div>` : ''}
|
|
123
240
|
</div>
|
|
124
241
|
${meta.eventId ? '<span class="griot-timeline-ref__arrow">→</span>' : ''}
|
|
@@ -133,69 +250,99 @@ function _render(block, opts) {
|
|
|
133
250
|
default: {
|
|
134
251
|
const el = document.createElement('p');
|
|
135
252
|
el.className = 'griot-block griot-paragraph';
|
|
136
|
-
el.
|
|
253
|
+
if (text) el.appendChild(il(text, opts));
|
|
137
254
|
return el;
|
|
138
255
|
}
|
|
139
256
|
}
|
|
140
257
|
}
|
|
141
258
|
|
|
259
|
+
// ─── Gallery renderer ─────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
function _renderGallery(items, layout, opts) {
|
|
262
|
+
const wrap = document.createElement('div');
|
|
263
|
+
wrap.className = `griot-block griot-gallery griot-gallery--${LAYOUT_OPTIONS.includes(layout)?layout:'grid'}`;
|
|
264
|
+
|
|
265
|
+
if (!items.length) {
|
|
266
|
+
wrap.innerHTML = `<div class="griot-gallery__empty">No images yet</div>`;
|
|
267
|
+
return wrap;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const [i, item] of items.entries()) {
|
|
271
|
+
const el = document.createElement('div');
|
|
272
|
+
el.className = 'griot-gallery__item';
|
|
273
|
+
const img = document.createElement('img');
|
|
274
|
+
img.src = item.src ?? item.url ?? '';
|
|
275
|
+
img.alt = item.alt ?? item.alt_text ?? '';
|
|
276
|
+
img.loading = 'lazy';
|
|
277
|
+
el.appendChild(img);
|
|
278
|
+
if (item.caption) {
|
|
279
|
+
const cap = document.createElement('p');
|
|
280
|
+
cap.className = 'griot-gallery__caption';
|
|
281
|
+
cap.textContent = item.caption;
|
|
282
|
+
el.appendChild(cap);
|
|
283
|
+
}
|
|
284
|
+
wrap.appendChild(el);
|
|
285
|
+
}
|
|
286
|
+
return wrap;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Citation renderer ────────────────────────────────────────────────────────
|
|
290
|
+
|
|
142
291
|
function _renderCitation(block, opts) {
|
|
143
292
|
const { meta = {} } = block;
|
|
144
293
|
const wrap = document.createElement('figure');
|
|
145
294
|
wrap.className = 'griot-block griot-citation';
|
|
146
295
|
|
|
147
|
-
if (!meta.bookId) {
|
|
148
|
-
wrap.innerHTML = `<div class="griot-citation__empty">📖 No source selected yet</div>`;
|
|
149
|
-
return wrap;
|
|
150
|
-
}
|
|
296
|
+
if (!meta.bookId) { wrap.innerHTML = `<div class="griot-citation__empty">📖 No source selected yet</div>`; return wrap; }
|
|
151
297
|
|
|
152
298
|
const book = (opts.books ?? []).find(b => b.id === meta.bookId);
|
|
153
299
|
const unit = book?.units?.find(u => u.id === meta.unitId);
|
|
154
|
-
|
|
155
|
-
if (!book || !unit) {
|
|
156
|
-
wrap.innerHTML = `<div class="griot-citation__missing">📖 Source not found — book may have been removed</div>`;
|
|
157
|
-
return wrap;
|
|
158
|
-
}
|
|
300
|
+
if (!book || !unit) { wrap.innerHTML = `<div class="griot-citation__missing">📖 Source not found</div>`; return wrap; }
|
|
159
301
|
|
|
160
302
|
const inner = document.createElement('div');
|
|
161
303
|
inner.className = 'griot-citation__inner';
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
${
|
|
170
|
-
<span class="griot-citation__unit">${escHtml(unit.label)}</span>
|
|
304
|
+
inner.innerHTML = `
|
|
305
|
+
<div class="griot-citation__header">
|
|
306
|
+
<span class="griot-citation__book-icon">📖</span>
|
|
307
|
+
<span class="griot-citation__book-title">${escHtml(book.title)}</span>
|
|
308
|
+
${book.author ? `<span class="griot-citation__author">${escHtml(book.author)}</span>` : ''}
|
|
309
|
+
<span class="griot-citation__unit">${escHtml(unit.label)}</span>
|
|
310
|
+
</div>
|
|
311
|
+
${meta.quote ? `<blockquote class="griot-citation__quote">${escHtml(meta.quote)}</blockquote>` : ''}
|
|
171
312
|
`;
|
|
172
|
-
inner.appendChild(hdr);
|
|
173
|
-
|
|
174
|
-
// Quote
|
|
175
|
-
if (meta.quote) {
|
|
176
|
-
const q = document.createElement('blockquote');
|
|
177
|
-
q.className = 'griot-citation__quote';
|
|
178
|
-
q.textContent = meta.quote;
|
|
179
|
-
inner.appendChild(q);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Note (supports inline syntax)
|
|
183
313
|
if (meta.note) {
|
|
184
314
|
const note = document.createElement('div');
|
|
185
315
|
note.className = 'griot-citation__note';
|
|
186
|
-
note.appendChild(
|
|
316
|
+
note.appendChild(renderInlineToDOM(meta.note, opts));
|
|
187
317
|
inner.appendChild(note);
|
|
188
318
|
}
|
|
189
|
-
|
|
190
319
|
wrap.appendChild(inner);
|
|
320
|
+
return wrap;
|
|
321
|
+
}
|
|
191
322
|
|
|
192
|
-
|
|
193
|
-
if (unit.content) {
|
|
194
|
-
const preview = document.createElement('div');
|
|
195
|
-
preview.className = 'griot-citation__preview';
|
|
196
|
-
preview.textContent = unit.content.slice(0, 180) + (unit.content.length > 180 ? '…' : '');
|
|
197
|
-
wrap.appendChild(preview);
|
|
198
|
-
}
|
|
323
|
+
// ─── Embed URL helpers ────────────────────────────────────────────────────────
|
|
199
324
|
|
|
200
|
-
|
|
325
|
+
function _ytEmbed(src) {
|
|
326
|
+
if (!src) return null;
|
|
327
|
+
const m = src.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|shorts\/|embed\/))([a-zA-Z0-9_-]{11})/);
|
|
328
|
+
return m ? `https://www.youtube.com/embed/${m[1]}?rel=0` : null;
|
|
329
|
+
}
|
|
330
|
+
function _vimeoEmbed(src) {
|
|
331
|
+
if (!src) return null;
|
|
332
|
+
const m = src.match(/vimeo\.com\/(\d+)/);
|
|
333
|
+
return m ? `https://player.vimeo.com/video/${m[1]}` : null;
|
|
201
334
|
}
|
|
335
|
+
function _spotifyEmbed(src) {
|
|
336
|
+
if (!src) return null;
|
|
337
|
+
const m = src.match(/open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/);
|
|
338
|
+
return m ? `https://open.spotify.com/embed/${m[1]}/${m[2]}` : null;
|
|
339
|
+
}
|
|
340
|
+
function _scEmbed(src) {
|
|
341
|
+
if (!src) return null;
|
|
342
|
+
if (src.includes('soundcloud.com/'))
|
|
343
|
+
return `https://w.soundcloud.com/player/?url=${encodeURIComponent(src)}&color=%236366f1&auto_play=false&hide_related=true&show_comments=false`;
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Export helpers for editor use
|
|
348
|
+
export { _ytEmbed as resolveYouTube, _vimeoEmbed as resolveVimeo, _spotifyEmbed as resolveSpotify, _scEmbed as resolveSoundCloud };
|
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
// ─── BlockSchema.js ───────────────────────────────────────────────────────────
|
|
2
|
+
// Single source of truth for all block types
|
|
3
|
+
// Fields: category, label, icon, slashLabel, hasText, hasInline, defaultMeta, placeholder
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
2
6
|
const SCHEMA = {
|
|
3
|
-
paragraph: { label:
|
|
4
|
-
heading: { label:
|
|
5
|
-
blockquote: { label:
|
|
6
|
-
callout: { label:
|
|
7
|
-
callout_warning: { label:
|
|
8
|
-
callout_tip: { label:
|
|
9
|
-
callout_danger: { label:
|
|
10
|
-
code: { label:
|
|
11
|
-
list_ul: { label:
|
|
12
|
-
list_ol: { label:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
paragraph: { category:'text', label:'Paragraph', icon:'¶', slashLabel:'Text', hasText:true, hasInline:true, defaultMeta:{}, placeholder:'Write something… **bold** *italic* `code` ==highlight==' },
|
|
8
|
+
heading: { category:'text', label:'Heading', icon:'H', slashLabel:'Heading', hasText:true, hasInline:false, defaultMeta:{ level:2 }, placeholder:'Heading…' },
|
|
9
|
+
blockquote: { category:'text', label:'Quote', icon:'❝', slashLabel:'Quote', hasText:true, hasInline:true, defaultMeta:{}, placeholder:'Quote…' },
|
|
10
|
+
callout: { category:'text', label:'Callout', icon:'💡', slashLabel:'Callout', hasText:true, hasInline:true, defaultMeta:{ icon:'💡' }, placeholder:'Callout text…' },
|
|
11
|
+
callout_warning: { category:'text', label:'Warning', icon:'⚠️', slashLabel:'Warning', hasText:true, hasInline:true, defaultMeta:{ icon:'⚠️' }, placeholder:'Warning message…' },
|
|
12
|
+
callout_tip: { category:'text', label:'Tip', icon:'✅', slashLabel:'Tip', hasText:true, hasInline:true, defaultMeta:{ icon:'✅' }, placeholder:'Tip or note…' },
|
|
13
|
+
callout_danger: { category:'text', label:'Danger', icon:'🚨', slashLabel:'Danger', hasText:true, hasInline:true, defaultMeta:{ icon:'🚨' }, placeholder:'Critical warning…' },
|
|
14
|
+
code: { category:'text', label:'Code', icon:'</>', slashLabel:'Code block', hasText:true, hasInline:false, defaultMeta:{ language:'' }, placeholder:'// code…' },
|
|
15
|
+
list_ul: { category:'text', label:'Bullet List', icon:'•', slashLabel:'Bullet list', hasText:true, hasInline:false, defaultMeta:{}, placeholder:'Item 1\nItem 2\nItem 3' },
|
|
16
|
+
list_ol: { category:'text', label:'Numbered List', icon:'1.', slashLabel:'Numbered list', hasText:true, hasInline:false, defaultMeta:{}, placeholder:'First item\nSecond item' },
|
|
17
|
+
|
|
18
|
+
image: { category:'media', label:'Image', icon:'🖼', slashLabel:'Image', hasText:false, hasInline:false, defaultMeta:{ src:'', alt:'', caption:'', width:'full' }, placeholder:null },
|
|
19
|
+
video: { category:'media', label:'Video', icon:'▶', slashLabel:'Video', hasText:false, hasInline:false, defaultMeta:{ src:'', caption:'', embedUrl:null, platform:null }, placeholder:null },
|
|
20
|
+
audio: { category:'media', label:'Audio', icon:'🎵', slashLabel:'Audio', hasText:false, hasInline:false, defaultMeta:{ src:'', caption:'', embedUrl:null, platform:null }, placeholder:null },
|
|
21
|
+
gallery: { category:'media', label:'Gallery', icon:'▦', slashLabel:'Gallery', hasText:false, hasInline:false, defaultMeta:{ items:[], layout:'grid' }, placeholder:null },
|
|
22
|
+
|
|
23
|
+
embed: { category:'embed', label:'Embed', icon:'⬡', slashLabel:'Embed / iframe', hasText:false, hasInline:false, defaultMeta:{ src:'', height:400, caption:'' }, placeholder:null },
|
|
24
|
+
|
|
25
|
+
table: { category:'structure', label:'Table', icon:'⊞', slashLabel:'Table', hasText:false, hasInline:false, defaultMeta:{ headers:['Column 1','Column 2'], rows:[['','']] }, placeholder:null },
|
|
26
|
+
divider: { category:'structure', label:'Divider', icon:'—', slashLabel:'Divider', hasText:false, hasInline:false, defaultMeta:{}, placeholder:null },
|
|
27
|
+
|
|
28
|
+
timeline_ref: { category:'structure', label:'Timeline Event', icon:'⏱', slashLabel:'Timeline event', hasText:false, hasInline:false, defaultMeta:{ eventId:'', eventTitle:'', note:'' }, placeholder:null },
|
|
29
|
+
book_citation: { category:'structure', label:'Book Citation', icon:'📖', slashLabel:'Book citation', hasText:false, hasInline:false, defaultMeta:{ bookId:'', unitId:'', quote:'', note:'' }, placeholder:null },
|
|
19
30
|
};
|
|
20
31
|
|
|
21
|
-
export function getBlockDef(type)
|
|
22
|
-
export function getAllTypes()
|
|
23
|
-
export function
|
|
32
|
+
export function getBlockDef(type) { return SCHEMA[type] ?? SCHEMA.paragraph; }
|
|
33
|
+
export function getAllTypes() { return Object.keys(SCHEMA); }
|
|
34
|
+
export function getTypesByCategory(cat) { return Object.entries(SCHEMA).filter(([,d]) => d.category === cat).map(([t]) => t); }
|
|
35
|
+
export function defaultMeta(type) { return { ...(SCHEMA[type]?.defaultMeta ?? {}) }; }
|
|
36
|
+
|
|
24
37
|
export default SCHEMA;
|