@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/README.md
CHANGED
|
@@ -1,179 +1,444 @@
|
|
|
1
1
|
# Griot
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
Built for structured historical document authoring — works standalone or embedded inside a larger app.
|
|
3
|
+
A lightweight, extensible block editor and viewer for the web. Inspired by Notion, built with plain JavaScript and zero dependencies. Griot ships a complete dark-themed CSS file, an immutable document model, and a fully keyboard-driven editing experience.
|
|
5
4
|
|
|
6
5
|
---
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **19 block types** across four categories: text, media, embed, and structure
|
|
10
|
+
- **Inline markup** — 12 token types parsed by a standalone lexer
|
|
11
|
+
- **Markdown shortcuts** — type `# `, `> `, `- `, ` ``` ` etc. to convert a block on the fly
|
|
12
|
+
- **Floating format toolbar** — appears on text selection; wraps with bold, italic, underline, strikethrough, inline code, highlight, link, or color
|
|
13
|
+
- **Slash command palette** — type `/` in any empty block; searchable, keyboard-navigable, grouped by category
|
|
14
|
+
- **Undo / redo** — linear history stack, up to 200 snapshots
|
|
15
|
+
- **Live inline preview** — rendered below every text block that supports inline syntax
|
|
16
|
+
- **Read-only viewer** — same document, no editing controls; supports highlight + scroll-to-block
|
|
17
|
+
- **Immutable document operations** — every mutation returns a new document object
|
|
18
|
+
- **Schema-driven** — all block types live in `BlockSchema.js`; easy to extend
|
|
19
|
+
- **Default CSS** — ships with `griot.css`; dark theme with CSS variables scoped to `.griot-editor` / `.griot-viewer`
|
|
20
|
+
- **Zero dependencies** — pure ES modules, no framework, no bundler required
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
9
25
|
|
|
10
26
|
```bash
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
npm install griot
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or via ES module directly:
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<script type="module">
|
|
34
|
+
import { Editor, Viewer } from './path/to/griot.js';
|
|
35
|
+
</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Styling
|
|
41
|
+
|
|
42
|
+
Griot ships a complete default stylesheet (`griot.css`). Import it once:
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<link rel="stylesheet" href="node_modules/griot/griot.css">
|
|
13
46
|
```
|
|
14
47
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
48
|
+
All styles are scoped to `.griot-editor` and `.griot-viewer`. Every value is a CSS variable — override any of them to theme Griot to your app:
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
:root {
|
|
52
|
+
--griot-bg: #060918;
|
|
53
|
+
--griot-surface: rgba(255,255,255,0.03);
|
|
54
|
+
--griot-surface-hover: rgba(255,255,255,0.055);
|
|
55
|
+
--griot-border: rgba(255,255,255,0.07);
|
|
56
|
+
--griot-border-focus: rgba(99,102,241,0.5);
|
|
57
|
+
--griot-accent: #6366f1;
|
|
58
|
+
--griot-accent-soft: rgba(99,102,241,0.12);
|
|
59
|
+
--griot-accent-text: #a5b4fc;
|
|
60
|
+
--griot-text: #e2e8f0;
|
|
61
|
+
--griot-text-muted: #64748b;
|
|
62
|
+
--griot-text-faint: #334155;
|
|
63
|
+
--griot-code-bg: rgba(0,0,0,0.45);
|
|
64
|
+
--griot-code-color: #a5f3fc;
|
|
65
|
+
--griot-font-body: system-ui, -apple-system, sans-serif;
|
|
66
|
+
--griot-font-mono: 'Fira Code', 'Cascadia Code', monospace;
|
|
67
|
+
--griot-font-serif: 'Georgia', 'Times New Roman', serif;
|
|
68
|
+
--griot-radius: 8px;
|
|
69
|
+
}
|
|
18
70
|
```
|
|
19
71
|
|
|
20
72
|
---
|
|
21
73
|
|
|
22
|
-
## Quick
|
|
74
|
+
## Quick Start
|
|
23
75
|
|
|
24
76
|
### Editor
|
|
25
77
|
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log('
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
onRequestBookPicker(blockId, callback) {
|
|
47
|
-
// Open your SourcePicker UI, then call:
|
|
48
|
-
// callback({ bookId, unitId, quote, note })
|
|
49
|
-
},
|
|
50
|
-
});
|
|
78
|
+
```html
|
|
79
|
+
<link rel="stylesheet" href="griot.css">
|
|
80
|
+
<div id="editor"></div>
|
|
81
|
+
|
|
82
|
+
<script type="module">
|
|
83
|
+
import { Editor, createDocument } from 'griot';
|
|
84
|
+
|
|
85
|
+
const editor = new Editor(document.getElementById('editor'), {
|
|
86
|
+
doc: createDocument([
|
|
87
|
+
{ id: 'b1', type: 'heading', text: 'Hello World', meta: { level: 1 } },
|
|
88
|
+
{ id: 'b2', type: 'paragraph', text: 'Start writing…' },
|
|
89
|
+
]),
|
|
90
|
+
books: [],
|
|
91
|
+
onChange: (doc) => console.log('changed', doc),
|
|
92
|
+
onEventClick: (eventId) => console.log('event', eventId),
|
|
93
|
+
onCiteClick: (blockId) => console.log('cite', blockId),
|
|
94
|
+
onRequestBookPicker: (blockId, cb) => cb({ bookId: 'b1', unitId: 'u1', quote: '', note: '' }),
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
51
97
|
```
|
|
52
98
|
|
|
53
99
|
### Viewer
|
|
54
100
|
|
|
55
|
-
```
|
|
56
|
-
|
|
101
|
+
```html
|
|
102
|
+
<div id="viewer"></div>
|
|
103
|
+
|
|
104
|
+
<script type="module">
|
|
105
|
+
import { Viewer } from 'griot';
|
|
57
106
|
|
|
58
|
-
const viewer = new Viewer(document.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
107
|
+
const viewer = new Viewer(document.getElementById('viewer'), {
|
|
108
|
+
doc: myDocument,
|
|
109
|
+
books: myBooks,
|
|
110
|
+
onEventClick: (eventId) => { /* … */ },
|
|
111
|
+
onCiteClick: (blockId) => { /* … */ },
|
|
112
|
+
highlightBlockId: 'b2',
|
|
113
|
+
});
|
|
65
114
|
|
|
66
|
-
//
|
|
67
|
-
|
|
115
|
+
viewer.setHighlight('b1'); // scroll to + 2.2s pulse-highlight
|
|
116
|
+
</script>
|
|
68
117
|
```
|
|
69
118
|
|
|
70
119
|
---
|
|
71
120
|
|
|
72
|
-
## Block
|
|
121
|
+
## Block Types
|
|
122
|
+
|
|
123
|
+
All 19 block types are defined in `BlockSchema.js`.
|
|
124
|
+
|
|
125
|
+
### Text (10 types)
|
|
126
|
+
|
|
127
|
+
| Type | Slash label | Notes |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `paragraph` | Text | Supports full inline markup; live preview strip below input |
|
|
130
|
+
| `heading` | Heading | Levels 1–6; level picker in editor toolbar |
|
|
131
|
+
| `blockquote` | Quote | Supports inline markup |
|
|
132
|
+
| `callout` | Callout | 💡 Customisable icon |
|
|
133
|
+
| `callout_warning` | Warning | ⚠️ |
|
|
134
|
+
| `callout_tip` | Tip | ✅ |
|
|
135
|
+
| `callout_danger` | Danger | 🚨 |
|
|
136
|
+
| `code` | Code block | Language input in toolbar; `pre` white-space; monospace |
|
|
137
|
+
| `list_ul` | Bullet list | One item per line; Enter inserts newline |
|
|
138
|
+
| `list_ol` | Numbered list | One item per line; Enter inserts newline |
|
|
139
|
+
|
|
140
|
+
### Media (4 types)
|
|
73
141
|
|
|
74
|
-
| Type |
|
|
75
|
-
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
142
|
+
| Type | Slash label | Notes |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `image` | Image | `src`, `alt`, `caption`, `width` (`full` etc.) |
|
|
145
|
+
| `video` | Video | Auto-embeds YouTube (incl. Shorts) and Vimeo; falls back to native `<video>` |
|
|
146
|
+
| `audio` | Audio | Auto-embeds Spotify (track/album/playlist/episode) and SoundCloud; falls back to native `<audio>` |
|
|
147
|
+
| `gallery` | Gallery | Multiple items; layout: `grid`, `masonry`, `carousel`, or `strip` |
|
|
148
|
+
|
|
149
|
+
### Embed (1 type)
|
|
150
|
+
|
|
151
|
+
| Type | Slash label | Notes |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `embed` | Embed / iframe | Generic `<iframe>` with configurable `height` and optional `caption` |
|
|
154
|
+
|
|
155
|
+
### Structure (4 types)
|
|
156
|
+
|
|
157
|
+
| Type | Slash label | Notes |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `table` | Table | Full WYSIWYG editor with add/remove rows and columns; inline markup in cells |
|
|
160
|
+
| `divider` | Divider | `<hr>` |
|
|
161
|
+
| `timeline_ref` | Timeline event | `eventId`, `eventTitle`, `note`; clickable in viewer → `onEventClick` |
|
|
162
|
+
| `book_citation` | Book citation | `bookId`, `unitId`, `quote`, `note`; triggers `onRequestBookPicker` |
|
|
85
163
|
|
|
86
164
|
---
|
|
87
165
|
|
|
88
|
-
## Inline
|
|
166
|
+
## Inline Markup
|
|
89
167
|
|
|
90
|
-
|
|
168
|
+
The inline parser (`InlineLexer.js`) is fully independent and can be used standalone. Twelve token types are supported, evaluated in priority order:
|
|
91
169
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
170
|
+
| Syntax | Token | Renders as |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `**bold**` | `BOLD` | `<strong>` |
|
|
173
|
+
| `*italic*` | `ITALIC` | `<em>` |
|
|
174
|
+
| `__underline__` | `UNDERLINE` | `<u>` |
|
|
175
|
+
| `~~strikethrough~~` | `STRIKE` | `<s>` |
|
|
176
|
+
| `` `code` `` | `CODE` | `<code class="griot-inline-code">` |
|
|
177
|
+
| `==highlight==` | `HIGHLIGHT` | `<mark class="griot-highlight">` |
|
|
178
|
+
| `{#f00:red}` or `{tomato:text}` | `COLOR_MARK` | `<span style="color:…">` |
|
|
179
|
+
| `[label](url)` | `LINK` | `<a class="griot-link" target="_blank">` |
|
|
180
|
+
| `` | `IMAGE` | `<img class="griot-inline-img">` |
|
|
181
|
+
| `[[event:id\|label]]` | `EVENT_REF` | Clickable chip → `onEventClick` |
|
|
182
|
+
| `[[cite:id\|label]]` | `CITE_REF` | Clickable chip → `onCiteClick` |
|
|
100
183
|
|
|
101
184
|
---
|
|
102
185
|
|
|
103
|
-
##
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
186
|
+
## Markdown Block Shortcuts
|
|
187
|
+
|
|
188
|
+
Typing these at the **start** of a block converts it instantly:
|
|
189
|
+
|
|
190
|
+
| Pattern | Converts to |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `# ` | Heading H1 |
|
|
193
|
+
| `## ` through `###### ` | Heading H2–H6 |
|
|
194
|
+
| `> ` | Blockquote |
|
|
195
|
+
| `- ` or `* ` | Bullet list |
|
|
196
|
+
| `1. ` | Numbered list |
|
|
197
|
+
| `--- ` | Divider (text cleared) |
|
|
198
|
+
| ` ``` ` or ` ``` ` + space | Code block |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Editor Keyboard Shortcuts
|
|
203
|
+
|
|
204
|
+
| Key | Action |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `Enter` | Split block at cursor (newline in list blocks) |
|
|
207
|
+
| `Backspace` at offset 0 | Merge block with previous; cursor placed at merge point |
|
|
208
|
+
| `Delete` at end | Merge next block into current |
|
|
209
|
+
| `↑` on first visual line | Move focus to previous block |
|
|
210
|
+
| `↓` on last visual line | Move focus to next block |
|
|
211
|
+
| `Ctrl/Cmd+Z` | Undo |
|
|
212
|
+
| `Ctrl/Cmd+Y` or `Ctrl/Cmd+Shift+Z` | Redo |
|
|
213
|
+
| `Ctrl/Cmd+B` | Wrap selection in `**…**` |
|
|
214
|
+
| `Ctrl/Cmd+I` | Wrap selection in `*…*` |
|
|
215
|
+
| `Ctrl/Cmd+U` | Wrap selection in `__…__` |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Concepts
|
|
220
|
+
|
|
221
|
+
### Document
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
interface Document {
|
|
225
|
+
id: string;
|
|
226
|
+
blocks: Block[];
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Block
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
interface Block {
|
|
234
|
+
id: string;
|
|
235
|
+
type: string;
|
|
236
|
+
text: string | null; // only present when hasText: true in schema
|
|
237
|
+
meta: Record<string, any>;
|
|
121
238
|
}
|
|
122
239
|
```
|
|
123
240
|
|
|
124
241
|
---
|
|
125
242
|
|
|
126
|
-
##
|
|
243
|
+
## API Reference
|
|
244
|
+
|
|
245
|
+
### Core
|
|
246
|
+
|
|
247
|
+
| Export | Description |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `createBlock(type, overrides?)` | New block with unique id |
|
|
250
|
+
| `cloneBlock(block, newId?)` | Deep clone; `newId` defaults to `true` |
|
|
251
|
+
| `isTextBlock(block)` | `true` if block has a text field |
|
|
252
|
+
| `isValidBlock(block)` | Minimal structural check |
|
|
253
|
+
| `anchorId(blockId)` | DOM `id` string for a block element |
|
|
254
|
+
| `scrollToBlock(blockId, behavior?)` | `scrollIntoView` wrapper |
|
|
255
|
+
| `TEXT_TYPES` | `Set<string>` of types that carry a text field |
|
|
256
|
+
| `ALL_TYPES` | `string[]` of all known types |
|
|
257
|
+
|
|
258
|
+
### Document Operations
|
|
259
|
+
|
|
260
|
+
All functions are immutable — they return a new document object.
|
|
261
|
+
|
|
262
|
+
| Export | Description |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `createDocument(blocks?)` | New document; falls back to a single empty paragraph |
|
|
265
|
+
| `toJSON(doc)` / `fromJSON(json)` | Serialize / deserialize |
|
|
266
|
+
| `getBlock(doc, id)` | Find a block by id |
|
|
267
|
+
| `getBlockIndex(doc, id)` | Index of a block |
|
|
268
|
+
| `getBlockBefore(doc, id)` / `getBlockAfter(doc, id)` | Adjacent blocks |
|
|
269
|
+
| `updateBlock(doc, id, patch)` | Patch `text`, `type`, and/or `meta` (meta is shallow-merged) |
|
|
270
|
+
| `insertBlockAfter(doc, afterId, block)` | Insert a block |
|
|
271
|
+
| `insertBlockBefore(doc, beforeId, block)` | Insert a block |
|
|
272
|
+
| `removeBlock(doc, id)` | Delete a block |
|
|
273
|
+
| `moveBlock(doc, fromIdx, toIdx)` | Reorder blocks |
|
|
274
|
+
| `splitBlock(doc, blockId, offset)` | Split at cursor offset; headings become paragraphs. Returns `[newDoc, newBlockId]` |
|
|
275
|
+
| `mergeBlockWithPrev(doc, blockId)` | Concatenate text with previous block. Returns `[newDoc, prevId, mergeOffset]` |
|
|
276
|
+
|
|
277
|
+
### Inline Parsing & Rendering
|
|
278
|
+
|
|
279
|
+
| Export | Description |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `tokenizeInline(text)` | Returns `Token[]` |
|
|
282
|
+
| `renderInlineToDOM(text, callbacks?)` | Returns a `DocumentFragment` |
|
|
283
|
+
| `renderInlineToHTML(text)` | Returns an HTML string |
|
|
284
|
+
| `escHtml(str)` / `escAttr(str)` | Escape helpers |
|
|
285
|
+
| `TOKEN` | Frozen enum of all token type strings |
|
|
127
286
|
|
|
128
|
-
|
|
287
|
+
### Block Rendering
|
|
129
288
|
|
|
130
|
-
|
|
131
|
-
|
|
289
|
+
| Export | Description |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `renderBlock(block, options)` | Renders a single block to a DOM element |
|
|
292
|
+
| `getBlockDef(type)` | Schema definition for a type |
|
|
293
|
+
| `getAllTypes()` | All registered type names |
|
|
294
|
+
| `getTypesByCategory(category)` | Types filtered by `'text'`, `'media'`, `'embed'`, or `'structure'` |
|
|
295
|
+
| `defaultMeta(type)` | Default meta object for a type |
|
|
296
|
+
| `resolveYouTube(src)` | Extracts YouTube embed URL |
|
|
297
|
+
| `resolveVimeo(src)` | Extracts Vimeo embed URL |
|
|
298
|
+
| `resolveSpotify(src)` | Extracts Spotify embed URL |
|
|
299
|
+
| `resolveSoundCloud(src)` | Builds SoundCloud player URL |
|
|
132
300
|
|
|
133
|
-
|
|
134
|
-
anchorId('b_abc123') // → "griot-b_abc123"
|
|
301
|
+
### Editor
|
|
135
302
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
303
|
+
```typescript
|
|
304
|
+
new Editor(container: HTMLElement, options: {
|
|
305
|
+
doc: Document;
|
|
306
|
+
books?: Book[];
|
|
307
|
+
onChange?: (doc: Document) => void; // debounced 400 ms while typing
|
|
308
|
+
onEventClick?: (eventId: string) => void;
|
|
309
|
+
onCiteClick?: (blockId: string) => void;
|
|
310
|
+
onRequestBookPicker?: (
|
|
311
|
+
blockId: string,
|
|
312
|
+
callback: (selection: { bookId: string; unitId: string; quote: string; note: string }) => void
|
|
313
|
+
) => void;
|
|
314
|
+
})
|
|
139
315
|
```
|
|
140
316
|
|
|
141
|
-
|
|
142
|
-
store `{ docId, blockId }` on a citation, then call `scrollToBlock(blockId)` when the timeline jumps to it.
|
|
317
|
+
**Methods:** `setDoc(doc)`, `setBooks(books)`, `focus(blockId)`, `destroy()`
|
|
143
318
|
|
|
144
|
-
|
|
319
|
+
**Per-block toolbar:** type switcher (all 19 types), heading level selector (H1–H6), code language input, move up/down, add below, delete.
|
|
320
|
+
|
|
321
|
+
**`onChange` debouncing:** while the user types, intermediate state is captured via `history.replace()`. A new undo point is committed 400 ms after the last keystroke.
|
|
322
|
+
|
|
323
|
+
### Viewer
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
new Viewer(container: HTMLElement, options: {
|
|
327
|
+
doc?: Document;
|
|
328
|
+
books?: Book[];
|
|
329
|
+
onEventClick?: (eventId: string) => void;
|
|
330
|
+
onCiteClick?: (blockId: string) => void;
|
|
331
|
+
highlightBlockId?: string;
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Methods:** `setDoc(doc)`, `setBooks(books)`, `setHighlight(blockId, options?)`, `destroy()`
|
|
336
|
+
|
|
337
|
+
`setHighlight` scrolls to the block and applies a 2.2-second CSS pulse animation, then removes the highlight class automatically.
|
|
145
338
|
|
|
146
|
-
|
|
339
|
+
### History
|
|
147
340
|
|
|
148
|
-
|
|
149
|
-
|
|
341
|
+
```javascript
|
|
342
|
+
import { History } from 'griot';
|
|
343
|
+
|
|
344
|
+
const history = new History(initialDoc); // max 200 snapshots
|
|
345
|
+
history.push(doc); // new undo point (discards redo future)
|
|
346
|
+
history.replace(doc); // overwrite current snapshot without a new undo point
|
|
347
|
+
history.undo(); // returns previous document
|
|
348
|
+
history.redo(); // returns next document
|
|
349
|
+
history.current; // current document
|
|
350
|
+
history.canUndo(); // boolean
|
|
351
|
+
history.canRedo(); // boolean
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Keyboard Helpers
|
|
355
|
+
|
|
356
|
+
| Export | Description |
|
|
150
357
|
|---|---|
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
358
|
+
| `attachKeyboardHandler(el, blockId, callbacks)` | Attach all editor key bindings to a `contenteditable` |
|
|
359
|
+
| `getCursorOffset(el)` | Character offset of caret |
|
|
360
|
+
| `setCursorOffset(el, offset)` | Move caret to character offset |
|
|
361
|
+
| `getSelectionOffsets(el)` | `{ start, end }` of current selection |
|
|
362
|
+
| `focusAtEnd(el)` / `focusAtStart(el)` | Move caret to end / start |
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Multimedia
|
|
367
|
+
|
|
368
|
+
### Gallery layouts
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
{ type: 'gallery', meta: { items: [{ src, alt, caption }, …], layout: 'grid' } }
|
|
372
|
+
// layout: 'grid' | 'masonry' | 'carousel' | 'strip'
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Auto-embed detection
|
|
376
|
+
|
|
377
|
+
Setting `meta.src` on a `video` or `audio` block automatically produces the right embed:
|
|
378
|
+
|
|
379
|
+
| URL pattern | Result |
|
|
160
380
|
|---|---|
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
381
|
+
| `youtube.com/watch?v=…`, `youtu.be/…`, `/shorts/…` | YouTube iframe |
|
|
382
|
+
| `vimeo.com/…` | Vimeo iframe |
|
|
383
|
+
| `open.spotify.com/track/…` (album/playlist/episode too) | Spotify iframe |
|
|
384
|
+
| `soundcloud.com/…` | SoundCloud widget |
|
|
385
|
+
| Anything else | Native `<video>` / `<audio>` |
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Extending
|
|
390
|
+
|
|
391
|
+
### Adding a Block Type
|
|
392
|
+
|
|
393
|
+
1. Add an entry to `BlockSchema.js` (or patch the schema at runtime).
|
|
394
|
+
2. Add a rendering case to `BlockRenderer.js`.
|
|
395
|
+
3. If it needs an editor UI, add a case to `Editor._buildSpecialBlockUI()`.
|
|
396
|
+
4. The slash menu reads from the schema — no changes needed there.
|
|
397
|
+
|
|
398
|
+
### Custom Inline Syntax
|
|
399
|
+
|
|
400
|
+
Add a rule to `InlineLexer.js`, then handle the new token type in both `InlineRenderer._toNode()` (DOM) and `InlineRenderer._toHTML()` (HTML string).
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Project Structure
|
|
405
|
+
|
|
179
406
|
```
|
|
407
|
+
src/
|
|
408
|
+
core/
|
|
409
|
+
Block.js — block primitives, TEXT_TYPES, uid, anchorId
|
|
410
|
+
Document.js — immutable document operations
|
|
411
|
+
History.js — undo/redo stack (max 200)
|
|
412
|
+
blocks/
|
|
413
|
+
BlockSchema.js — single source of truth for all 19 block types
|
|
414
|
+
BlockRenderer.js — block → DOM element (used by Viewer)
|
|
415
|
+
inline/
|
|
416
|
+
InlineLexer.js — tokenizer (12 token types)
|
|
417
|
+
InlineRenderer.js — tokens → DOM fragment or HTML string
|
|
418
|
+
editor/
|
|
419
|
+
Editor.js — full editing lifecycle
|
|
420
|
+
FormatToolbar.js — floating selection toolbar
|
|
421
|
+
SlashMenu.js — slash command palette
|
|
422
|
+
Keyboard.js — key bindings and cursor helpers
|
|
423
|
+
viewer/
|
|
424
|
+
Viewer.js — read-only renderer
|
|
425
|
+
griot.js — public entry point
|
|
426
|
+
griot.css — default dark theme (CSS variables)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Development
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
git clone https://github.com/yourname/griot.git
|
|
435
|
+
cd griot
|
|
436
|
+
npm install
|
|
437
|
+
npm run dev # dev server at localhost:5000
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## License
|
|
443
|
+
|
|
444
|
+
MIT
|
package/package.json
CHANGED