@0m0g1/griot 0.1.0

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 ADDED
@@ -0,0 +1,179 @@
1
+ # Griot
2
+
3
+ A self-contained block-based rich text editor and renderer.
4
+ Built for structured historical document authoring — works standalone or embedded inside a larger app.
5
+
6
+ ---
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ # Copy src/ into your project, or:
12
+ npm install griot # (once published)
13
+ ```
14
+
15
+ ```js
16
+ import 'griot/css'; // styles
17
+ import { Editor, Viewer, createDocument } from 'griot';
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Quick start
23
+
24
+ ### Editor
25
+
26
+ ```js
27
+ import { Editor, createDocument } from 'griot';
28
+ import 'griot/css';
29
+
30
+ const doc = createDocument('My Article');
31
+
32
+ const editor = new Editor(document.querySelector('#editor'), {
33
+ doc,
34
+ books: [], // optional: parsed books for citations
35
+ onChange(updatedDoc) {
36
+ localStorage.setItem('draft', JSON.stringify(updatedDoc));
37
+ },
38
+ onEventClick(eventId) {
39
+ // e.g. AppShell.handleSelectItemById(eventId)
40
+ console.log('Open timeline event:', eventId);
41
+ },
42
+ onCiteClick(blockId) {
43
+ // scroll viewer to that block
44
+ viewer.setHighlight(blockId);
45
+ },
46
+ onRequestBookPicker(blockId, callback) {
47
+ // Open your SourcePicker UI, then call:
48
+ // callback({ bookId, unitId, quote, note })
49
+ },
50
+ });
51
+ ```
52
+
53
+ ### Viewer
54
+
55
+ ```js
56
+ import { Viewer } from 'griot';
57
+
58
+ const viewer = new Viewer(document.querySelector('#viewer'), {
59
+ doc,
60
+ books: [],
61
+ onEventClick(eventId) {
62
+ console.log('Open event:', eventId);
63
+ },
64
+ });
65
+
66
+ // Jump to a block (e.g. from a timeline citation)
67
+ viewer.setHighlight('b_abc123');
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Block types
73
+
74
+ | Type | Icon | Text field | Notes |
75
+ |---|---|---|---|
76
+ | `paragraph` | ¶ | ✓ | Inline syntax supported |
77
+ | `heading` | H | ✓ | `meta.level` 1–6 |
78
+ | `blockquote` | ❝ | ✓ | Inline syntax supported |
79
+ | `callout` | 💡 | ✓ | `meta.icon` for the emoji |
80
+ | `code` | </> | ✓ | No inline parsing. `meta.language` for highlight |
81
+ | `divider` | — | — | Horizontal rule |
82
+ | `image` | 🖼 | — | `meta.src`, `meta.alt`, `meta.caption` |
83
+ | `timeline_ref` | ⏱ | — | `meta.eventId`, `meta.eventTitle`, `meta.note` |
84
+ | `book_citation` | 📖 | — | `meta.bookId`, `meta.unitId`, `meta.quote`, `meta.note` |
85
+
86
+ ---
87
+
88
+ ## Inline syntax
89
+
90
+ Works inside any block with `hasInline: true` (paragraph, blockquote, callout, note fields):
91
+
92
+ ```
93
+ **bold**
94
+ *italic*
95
+ `inline code`
96
+ [link text](https://example.com)
97
+ [[event:rome_founding|The Founding of Rome]] → timeline event chip
98
+ [[cite:b_abc123|See Chapter 2]] → citation cross-reference
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Document format (`.griot.json`)
104
+
105
+ ```json
106
+ {
107
+ "version": 1,
108
+ "id": "doc_abc",
109
+ "title": "The Fall of Rome",
110
+ "createdAt": "2025-01-01T00:00:00.000Z",
111
+ "updatedAt": "2025-01-01T00:00:00.000Z",
112
+ "blocks": [
113
+ { "id": "b_1", "type": "heading", "text": "The Fall of Rome", "meta": { "level": 1 } },
114
+ { "id": "b_2", "type": "paragraph", "text": "In **476 CE** the last emperor [[event:fall_of_rome|was deposed]].", "meta": {} },
115
+ { "id": "b_3", "type": "book_citation", "text": null, "meta": {
116
+ "bookId": "book_xyz", "unitId": "unit_abc",
117
+ "quote": "The barbarians had long served in Roman armies.",
118
+ "note": "Essential context for understanding the transition."
119
+ }}
120
+ ]
121
+ }
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Deep-link anchors
127
+
128
+ Every rendered block gets `id="griot-{blockId}"` in the DOM.
129
+
130
+ ```js
131
+ import { anchorId, scrollToBlock } from 'griot';
132
+
133
+ // Get the DOM id for a block
134
+ anchorId('b_abc123') // → "griot-b_abc123"
135
+
136
+ // Scroll to a block (viewer or editor)
137
+ scrollToBlock('b_abc123');
138
+ scrollToBlock('b_abc123', 'instant');
139
+ ```
140
+
141
+ This is the contract for timeline → article navigation:
142
+ store `{ docId, blockId }` on a citation, then call `scrollToBlock(blockId)` when the timeline jumps to it.
143
+
144
+ ---
145
+
146
+ ## API reference
147
+
148
+ ### `Editor`
149
+ | Method | Description |
150
+ |---|---|
151
+ | `new Editor(el, options)` | Mount editor into `el` |
152
+ | `editor.doc` | Current document (read-only) |
153
+ | `editor.setDoc(doc)` | Replace document |
154
+ | `editor.setBooks(books)` | Update available books |
155
+ | `editor.focus(blockId)` | Focus a specific block |
156
+ | `editor.destroy()` | Unmount and clean up |
157
+
158
+ ### `Viewer`
159
+ | Method | Description |
160
+ |---|---|
161
+ | `new Viewer(el, options)` | Mount viewer into `el` |
162
+ | `viewer.setDoc(doc)` | Replace document |
163
+ | `viewer.setBooks(books)` | Update available books |
164
+ | `viewer.setHighlight(blockId)` | Scroll to + briefly highlight a block |
165
+ | `viewer.destroy()` | Unmount and clean up |
166
+
167
+ ### Document helpers
168
+ ```js
169
+ createDocument(title)
170
+ createBlock(type, overrides)
171
+ updateBlock(doc, blockId, patch)
172
+ insertBlockAfter(doc, blockId, newBlock)
173
+ removeBlock(doc, blockId)
174
+ splitBlock(doc, blockId, offset) // returns [newDoc, newBlockId]
175
+ mergeBlockWithPrev(doc, blockId) // returns [newDoc, prevId, offset]
176
+ moveBlock(doc, fromIndex, toIndex)
177
+ toJSON(doc)
178
+ fromJSON(jsonStringOrObject)
179
+ ```
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@0m0g1/griot",
3
+ "version": "0.1.0",
4
+ "description": "A self-contained block-based rich text editor and renderer built for historical document authoring.",
5
+ "type": "module",
6
+ "main": "./src/Griot.js",
7
+ "module": "./src/Griot.js",
8
+ "exports": {
9
+ ".": "./src/Griot.js",
10
+ "./css": "./src/griot.css"
11
+ },
12
+ "files": [
13
+ "src/"
14
+ ],
15
+ "keywords": ["rich-text", "editor", "blocks", "history", "document"],
16
+ "license": "MIT"
17
+ }
package/src/Griot.js ADDED
@@ -0,0 +1,54 @@
1
+ // ─── Griot.js ─────────────────────────────────────────────────────────────────
2
+ // Public facade. Import from here — never from internal modules directly.
3
+ //
4
+ // Named exports cover every public surface:
5
+ //
6
+ // Classes
7
+ // Editor, Viewer
8
+ //
9
+ // Document model
10
+ // createDocument, createBlock, cloneBlock
11
+ // updateBlock, insertBlockAfter, insertBlockBefore,
12
+ // removeBlock, splitBlock, mergeBlockWithPrev, moveBlock,
13
+ // getBlock, getBlockIndex, toJSON, fromJSON
14
+ //
15
+ // Block helpers
16
+ // anchorId, scrollToBlock, isTextBlock
17
+ //
18
+ // Inline
19
+ // tokenizeInline, renderInlineToDOM, renderInlineToHTML, TOKEN
20
+ //
21
+ // Schema
22
+ // getBlockDef, getAllTypes, defaultMeta, BlockSchema
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ // Core
26
+ export {
27
+ createBlock, cloneBlock, isTextBlock, isValidBlock,
28
+ anchorId, scrollToBlock,
29
+ TEXT_TYPES, ALL_TYPES,
30
+ } from './core/Block.js';
31
+
32
+ export {
33
+ createDocument, toJSON, fromJSON,
34
+ getBlock, getBlockIndex, getBlockAfter, getBlockBefore,
35
+ updateBlock, insertBlockAfter, insertBlockBefore,
36
+ removeBlock, moveBlock, splitBlock, mergeBlockWithPrev,
37
+ } from './core/Document.js';
38
+
39
+ export { History } from './core/History.js';
40
+
41
+ // Inline
42
+ export { tokenizeInline, TOKEN } from './inline/InlineLexer.js';
43
+ export {
44
+ renderInlineToDOM, renderInlineToHTML, escHtml, escAttr,
45
+ } from './inline/InlineRenderer.js';
46
+
47
+ // Blocks
48
+ export { getBlockDef, getAllTypes, defaultMeta } from './blocks/BlockSchema.js';
49
+ export { default as BlockSchema } from './blocks/BlockSchema.js';
50
+ export { renderBlock } from './blocks/BlockRenderer.js';
51
+
52
+ // Editor / Viewer
53
+ export { Editor } from './editor/Editor.js';
54
+ export { Viewer } from './viewer/Viewer.js';
@@ -0,0 +1,201 @@
1
+ // ─── BlockRenderer.js ─────────────────────────────────────────────────────────
2
+ // Renders a single block to a DOM element.
3
+ // Used by both Viewer (read-only) and Editor (preview layer).
4
+ //
5
+ // Options:
6
+ // books — array of parsed book objects (for book_citation)
7
+ // onEventClick — (eventId) => void
8
+ // onCiteClick — (blockId) => void
9
+ // editable — if true, skips event listeners (Editor manages them)
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ import { anchorId } from '../core/Block.js';
13
+ import { renderInlineToDOM, renderInlineToHTML, escHtml, escAttr } from '../inline/InlineRenderer.js';
14
+ import { getBlockDef } from './BlockSchema.js';
15
+
16
+ // ─── Public entry point ───────────────────────────────────────────────────────
17
+ export function renderBlock(block, { books = [], onEventClick, onCiteClick } = {}) {
18
+ const el = _render(block, { books, onEventClick, onCiteClick });
19
+ if (el) {
20
+ el.id = anchorId(block.id);
21
+ el.dataset.blockId = block.id;
22
+ el.dataset.blockType = block.type;
23
+ }
24
+ return el;
25
+ }
26
+
27
+ // ─── Internal ─────────────────────────────────────────────────────────────────
28
+ function inlineDOM(text, opts) {
29
+ return renderInlineToDOM(text, {
30
+ onEventClick: opts.onEventClick,
31
+ onCiteClick: opts.onCiteClick,
32
+ });
33
+ }
34
+
35
+ function _render(block, opts) {
36
+ const { text, meta = {}, type } = block;
37
+
38
+ switch (type) {
39
+
40
+ case 'paragraph': {
41
+ const el = document.createElement('p');
42
+ el.className = 'griot-block griot-paragraph';
43
+ if (text) el.appendChild(inlineDOM(text, opts));
44
+ return el;
45
+ }
46
+
47
+ case 'heading': {
48
+ const level = Math.max(1, Math.min(6, meta.level ?? 2));
49
+ const el = document.createElement(`h${level}`);
50
+ el.className = `griot-block griot-heading griot-heading--${level}`;
51
+ el.textContent = text ?? '';
52
+ return el;
53
+ }
54
+
55
+ case 'blockquote': {
56
+ const el = document.createElement('blockquote');
57
+ el.className = 'griot-block griot-blockquote';
58
+ if (text) el.appendChild(inlineDOM(text, opts));
59
+ return el;
60
+ }
61
+
62
+ case 'callout': {
63
+ const el = document.createElement('div');
64
+ const icon = document.createElement('span');
65
+ const body = document.createElement('div');
66
+ el.className = 'griot-block griot-callout';
67
+ icon.className = 'griot-callout__icon';
68
+ body.className = 'griot-callout__body';
69
+ icon.textContent = meta.icon ?? '💡';
70
+ if (text) body.appendChild(inlineDOM(text, opts));
71
+ el.appendChild(icon);
72
+ el.appendChild(body);
73
+ return el;
74
+ }
75
+
76
+ case 'code': {
77
+ const pre = document.createElement('pre');
78
+ const code = document.createElement('code');
79
+ pre.className = 'griot-block griot-code';
80
+ if (meta.language) code.className = `language-${meta.language}`;
81
+ code.textContent = text ?? '';
82
+ pre.appendChild(code);
83
+ return pre;
84
+ }
85
+
86
+ case 'divider': {
87
+ const el = document.createElement('hr');
88
+ el.className = 'griot-block griot-divider';
89
+ return el;
90
+ }
91
+
92
+ case 'image': {
93
+ const figure = document.createElement('figure');
94
+ const img = document.createElement('img');
95
+ figure.className = 'griot-block griot-image';
96
+ img.src = meta.src ?? '';
97
+ img.alt = meta.alt ?? '';
98
+ figure.appendChild(img);
99
+ if (meta.caption) {
100
+ const cap = document.createElement('figcaption');
101
+ cap.textContent = meta.caption;
102
+ figure.appendChild(cap);
103
+ }
104
+ return figure;
105
+ }
106
+
107
+ case 'timeline_ref': {
108
+ const el = document.createElement('div');
109
+ el.className = 'griot-block griot-timeline-ref';
110
+ if (meta.eventId && opts.onEventClick) {
111
+ el.setAttribute('role', 'button');
112
+ el.tabIndex = 0;
113
+ el.addEventListener('click', () => opts.onEventClick(meta.eventId));
114
+ el.addEventListener('keydown', (e) => {
115
+ if (e.key === 'Enter' || e.key === ' ') opts.onEventClick(meta.eventId);
116
+ });
117
+ }
118
+ el.innerHTML = `
119
+ <span class="griot-timeline-ref__icon">⏱</span>
120
+ <div class="griot-timeline-ref__body">
121
+ <div class="griot-timeline-ref__title">${escHtml(meta.eventTitle || 'Timeline Event')}</div>
122
+ ${meta.note ? `<div class="griot-timeline-ref__note">${escHtml(meta.note)}</div>` : ''}
123
+ </div>
124
+ ${meta.eventId ? '<span class="griot-timeline-ref__arrow">→</span>' : ''}
125
+ `;
126
+ return el;
127
+ }
128
+
129
+ case 'book_citation': {
130
+ return _renderCitation(block, opts);
131
+ }
132
+
133
+ default: {
134
+ const el = document.createElement('p');
135
+ el.className = 'griot-block griot-paragraph';
136
+ el.textContent = text ?? '';
137
+ return el;
138
+ }
139
+ }
140
+ }
141
+
142
+ function _renderCitation(block, opts) {
143
+ const { meta = {} } = block;
144
+ const wrap = document.createElement('figure');
145
+ wrap.className = 'griot-block griot-citation';
146
+
147
+ if (!meta.bookId) {
148
+ wrap.innerHTML = `<div class="griot-citation__empty">📖 No source selected yet</div>`;
149
+ return wrap;
150
+ }
151
+
152
+ const book = (opts.books ?? []).find(b => b.id === meta.bookId);
153
+ 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
+ }
159
+
160
+ const inner = document.createElement('div');
161
+ inner.className = 'griot-citation__inner';
162
+
163
+ // Header
164
+ const hdr = document.createElement('div');
165
+ hdr.className = 'griot-citation__header';
166
+ hdr.innerHTML = `
167
+ <span class="griot-citation__book-icon">📖</span>
168
+ <span class="griot-citation__book-title">${escHtml(book.title)}</span>
169
+ ${book.author ? `<span class="griot-citation__author">${escHtml(book.author)}</span>` : ''}
170
+ <span class="griot-citation__unit">${escHtml(unit.label)}</span>
171
+ `;
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
+ if (meta.note) {
184
+ const note = document.createElement('div');
185
+ note.className = 'griot-citation__note';
186
+ note.appendChild(inlineDOM(meta.note, opts));
187
+ inner.appendChild(note);
188
+ }
189
+
190
+ wrap.appendChild(inner);
191
+
192
+ // Content preview
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
+ }
199
+
200
+ return wrap;
201
+ }
@@ -0,0 +1,94 @@
1
+ // ─── BlockSchema.js ───────────────────────────────────────────────────────────
2
+ // The single source of truth for what block types exist, their labels,
3
+ // icons, whether they have text, and any default meta values.
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ const SCHEMA = {
7
+ paragraph: {
8
+ label: 'Paragraph',
9
+ icon: '¶',
10
+ hasText: true,
11
+ hasInline: true,
12
+ defaultMeta: {},
13
+ placeholder: 'Write something… **bold** *italic* `code` [[event:id|label]]',
14
+ },
15
+ heading: {
16
+ label: 'Heading',
17
+ icon: 'H',
18
+ hasText: true,
19
+ hasInline: false, // headings render as plain text, no inline chips
20
+ defaultMeta: { level: 2 },
21
+ placeholder: 'Heading…',
22
+ },
23
+ blockquote: {
24
+ label: 'Quote',
25
+ icon: '❝',
26
+ hasText: true,
27
+ hasInline: true,
28
+ defaultMeta: {},
29
+ placeholder: 'Quote…',
30
+ },
31
+ callout: {
32
+ label: 'Callout',
33
+ icon: '💡',
34
+ hasText: true,
35
+ hasInline: true,
36
+ defaultMeta: { icon: '💡' },
37
+ placeholder: 'Callout text…',
38
+ },
39
+ code: {
40
+ label: 'Code',
41
+ icon: '</>',
42
+ hasText: true,
43
+ hasInline: false, // code blocks are raw, no inline parsing
44
+ defaultMeta: { language: '' },
45
+ placeholder: '// code…',
46
+ },
47
+ divider: {
48
+ label: 'Divider',
49
+ icon: '—',
50
+ hasText: false,
51
+ hasInline: false,
52
+ defaultMeta: {},
53
+ placeholder: null,
54
+ },
55
+ image: {
56
+ label: 'Image',
57
+ icon: '🖼',
58
+ hasText: false,
59
+ hasInline: false,
60
+ defaultMeta: { src: '', alt: '', caption: '' },
61
+ placeholder: null,
62
+ },
63
+ timeline_ref: {
64
+ label: 'Timeline Event',
65
+ icon: '⏱',
66
+ hasText: false,
67
+ hasInline: false,
68
+ defaultMeta: { eventId: '', eventTitle: '', note: '' },
69
+ placeholder: null,
70
+ },
71
+ book_citation: {
72
+ label: 'Book Citation',
73
+ icon: '📖',
74
+ hasText: false,
75
+ hasInline: false,
76
+ defaultMeta: { bookId: '', unitId: '', quote: '', note: '' },
77
+ placeholder: null,
78
+ },
79
+ };
80
+
81
+ export function getBlockDef(type) {
82
+ return SCHEMA[type] ?? SCHEMA.paragraph;
83
+ }
84
+
85
+ export function getAllTypes() {
86
+ return Object.keys(SCHEMA);
87
+ }
88
+
89
+ // Returns default meta for a type (shallow copy)
90
+ export function defaultMeta(type) {
91
+ return { ...(SCHEMA[type]?.defaultMeta ?? {}) };
92
+ }
93
+
94
+ export default SCHEMA;
@@ -0,0 +1,63 @@
1
+ // ─── Block.js ─────────────────────────────────────────────────────────────────
2
+ // Pure data. No DOM, no rendering. A block is a plain serialisable object.
3
+ //
4
+ // Shape:
5
+ // { id, type, text?, meta:{} }
6
+ //
7
+ // meta holds type-specific fields:
8
+ // heading → meta.level (1-6)
9
+ // callout → meta.icon
10
+ // code → meta.language
11
+ // image → meta.src, meta.alt, meta.caption
12
+ // divider → (no extra fields)
13
+ // timeline_ref → meta.eventId, meta.eventTitle
14
+ // book_citation → meta.bookId, meta.unitId, meta.quote, meta.note
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+
17
+ let _seq = 0;
18
+ const uid = (prefix = 'b') => `${prefix}_${Date.now()}_${(++_seq).toString(36)}`;
19
+
20
+ // TEXT_TYPES — block types that carry a user-editable `text` field
21
+ export const TEXT_TYPES = new Set([
22
+ 'paragraph', 'heading', 'blockquote', 'callout', 'code',
23
+ ]);
24
+
25
+ // ALL_TYPES — exhaustive list for validation
26
+ export const ALL_TYPES = new Set([
27
+ 'paragraph', 'heading', 'blockquote', 'callout', 'code',
28
+ 'divider', 'image', 'timeline_ref', 'book_citation',
29
+ ]);
30
+
31
+ // ─── Factory ──────────────────────────────────────────────────────────────────
32
+ export function createBlock(type = 'paragraph', overrides = {}) {
33
+ if (!ALL_TYPES.has(type)) {
34
+ console.warn(`[Griot] Unknown block type "${type}", defaulting to paragraph`);
35
+ type = 'paragraph';
36
+ }
37
+ return {
38
+ id: uid('b'),
39
+ type,
40
+ text: TEXT_TYPES.has(type) ? '' : null,
41
+ meta: {},
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ export function cloneBlock(block) {
47
+ return {
48
+ ...block,
49
+ id: uid('b'),
50
+ meta: { ...block.meta },
51
+ };
52
+ }
53
+
54
+ // ─── Predicates ───────────────────────────────────────────────────────────────
55
+ export const isTextBlock = (b) => TEXT_TYPES.has(b?.type);
56
+ export const isValidBlock = (b) => b && typeof b.id === 'string' && ALL_TYPES.has(b.type);
57
+
58
+ // ─── Anchor ID (stable DOM id for deep-linking) ───────────────────────────────
59
+ export const anchorId = (blockId) => `griot-${blockId}`;
60
+ export const scrollToBlock = (blockId, behavior = 'smooth') => {
61
+ document.getElementById(anchorId(blockId))
62
+ ?.scrollIntoView({ behavior, block: 'center' });
63
+ };