@0m0g1/griot 0.1.2 → 0.1.4
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 +322 -127
- package/package.json +1 -1
- package/src/Griot.js +33 -35
- package/src/blocks/BlockRenderer.js +240 -93
- package/src/blocks/BlockSchema.js +29 -86
- package/src/core/Block.js +42 -45
- package/src/core/Document.js +63 -98
- package/src/core/History.js +17 -42
- package/src/editor/Editor.js +405 -138
- package/src/editor/FormatToolbar.js +138 -0
- package/src/editor/Keyboard.js +92 -106
- package/src/editor/SlashMenu.js +197 -0
- package/src/inline/InlineLexer.js +69 -72
- package/src/inline/InlineRenderer.js +110 -95
|
@@ -1,90 +1,87 @@
|
|
|
1
1
|
// ─── InlineLexer.js ───────────────────────────────────────────────────────────
|
|
2
|
-
// Tokenises
|
|
3
|
-
// Runs on a single text string (the content of one block's text field).
|
|
2
|
+
// Tokenises a plain-text string that uses lightweight inline markup.
|
|
4
3
|
//
|
|
5
|
-
// Supported syntax
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
4
|
+
// Supported syntax
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// **bold** → TOKEN.BOLD { text }
|
|
7
|
+
// *italic* → TOKEN.ITALIC { text }
|
|
8
|
+
// __underline__ → TOKEN.UNDERLINE { text }
|
|
9
|
+
// ~~strikethrough~~ → TOKEN.STRIKE { text }
|
|
10
|
+
// `inline code` → TOKEN.CODE { code }
|
|
11
|
+
// ==highlight== → TOKEN.HIGHLIGHT { text }
|
|
12
|
+
// {#f00:red text} {blue:text} → TOKEN.COLOR_MARK { color, text }
|
|
13
|
+
// [label](url) → TOKEN.LINK { label, href }
|
|
14
|
+
//  → TOKEN.IMAGE { alt, src }
|
|
15
|
+
// [[event:id|label]] → TOKEN.EVENT_REF { eventId, label }
|
|
16
|
+
// [[cite:blockId|label]] → TOKEN.CITE_REF { blockId, label }
|
|
17
|
+
// plain text → TOKEN.TEXT { text }
|
|
18
|
+
//
|
|
19
|
+
// Stateless and re-entrant. Rules are anchored regexes in priority order.
|
|
13
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
21
|
|
|
15
22
|
export const TOKEN = Object.freeze({
|
|
16
|
-
TEXT:
|
|
17
|
-
BOLD:
|
|
18
|
-
ITALIC:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
TEXT: 'text',
|
|
24
|
+
BOLD: 'bold',
|
|
25
|
+
ITALIC: 'italic',
|
|
26
|
+
UNDERLINE: 'underline',
|
|
27
|
+
STRIKE: 'strike',
|
|
28
|
+
CODE: 'code',
|
|
29
|
+
LINK: 'link',
|
|
30
|
+
IMAGE: 'image',
|
|
31
|
+
HIGHLIGHT: 'highlight',
|
|
32
|
+
COLOR_MARK: 'color_mark',
|
|
33
|
+
EVENT_REF: 'event_ref',
|
|
34
|
+
CITE_REF: 'cite_ref',
|
|
23
35
|
});
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
[
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const RULES = [
|
|
38
|
+
// Inline image  — must precede link rule
|
|
39
|
+
{ type: TOKEN.IMAGE, re: /^!\[([^\]]*)\]\(([^)\s]+)\)/, build: m => ({ alt: m[1], src: m[2] }) },
|
|
40
|
+
// Link [label](url)
|
|
41
|
+
{ type: TOKEN.LINK, re: /^\[([^\]]+)\]\(([^)\s]+)\)/, build: m => ({ label: m[1], href: m[2] }) },
|
|
42
|
+
// Bold **text** — before italic
|
|
43
|
+
{ type: TOKEN.BOLD, re: /^\*\*((?:[^*]|\*(?!\*))+)\*\*/, build: m => ({ text: m[1] }) },
|
|
44
|
+
// Italic *text*
|
|
45
|
+
{ type: TOKEN.ITALIC, re: /^\*((?:[^*])+)\*/, build: m => ({ text: m[1] }) },
|
|
46
|
+
// Underline __text__
|
|
47
|
+
{ type: TOKEN.UNDERLINE, re: /^__((?:[^_])+)__/, build: m => ({ text: m[1] }) },
|
|
48
|
+
// Strikethrough ~~text~~
|
|
49
|
+
{ type: TOKEN.STRIKE, re: /^~~((?:[^~])+)~~/, build: m => ({ text: m[1] }) },
|
|
50
|
+
// Highlight ==text==
|
|
51
|
+
{ type: TOKEN.HIGHLIGHT, re: /^==((?:[^=])+)==/, build: m => ({ text: m[1] }) },
|
|
52
|
+
// Colour mark {#hex:text} or {colorname:text}
|
|
53
|
+
{ type: TOKEN.COLOR_MARK, re: /^\{(#[0-9a-fA-F]{3,8}|[a-zA-Z][a-zA-Z0-9_-]*):([^}]+)\}/, build: m => ({ color: m[1], text: m[2] }) },
|
|
54
|
+
// Inline code `code`
|
|
55
|
+
{ type: TOKEN.CODE, re: /^`([^`]+)`/, build: m => ({ code: m[1] }) },
|
|
56
|
+
// Event ref [[event:id|label]]
|
|
57
|
+
{ type: TOKEN.EVENT_REF, re: /^\[\[event:([^\]|]+)(?:\|([^\]]*))?\]\]/, build: m => ({ eventId: m[1], label: m[2] || m[1] }) },
|
|
58
|
+
// Cite ref [[cite:id|label]]
|
|
59
|
+
{ type: TOKEN.CITE_REF, re: /^\[\[cite:([^\]|]+)(?:\|([^\]]*))?\]\]/, build: m => ({ blockId: m[1], label: m[2] || m[1] }) },
|
|
60
|
+
];
|
|
37
61
|
|
|
38
62
|
export function tokenizeInline(text = '') {
|
|
39
63
|
if (!text) return [];
|
|
40
|
-
|
|
41
64
|
const tokens = [];
|
|
42
|
-
let
|
|
43
|
-
let m;
|
|
65
|
+
let pos = 0, textStart = 0;
|
|
44
66
|
|
|
45
|
-
|
|
67
|
+
while (pos < text.length) {
|
|
68
|
+
const remaining = text.slice(pos);
|
|
69
|
+
let matched = false;
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
tokens.push({ type: TOKEN.TEXT, text: text.slice(
|
|
71
|
+
for (const rule of RULES) {
|
|
72
|
+
const m = remaining.match(rule.re);
|
|
73
|
+
if (!m) continue;
|
|
74
|
+
if (pos > textStart) tokens.push({ type: TOKEN.TEXT, text: text.slice(textStart, pos) });
|
|
75
|
+
tokens.push({ type: rule.type, ...rule.build(m) });
|
|
76
|
+
pos += m[0].length;
|
|
77
|
+
textStart = pos;
|
|
78
|
+
matched = true;
|
|
79
|
+
break;
|
|
51
80
|
}
|
|
52
81
|
|
|
53
|
-
if (
|
|
54
|
-
// bold
|
|
55
|
-
tokens.push({ type: TOKEN.BOLD, text: m[2] });
|
|
56
|
-
} else if (m[3]) {
|
|
57
|
-
// italic
|
|
58
|
-
tokens.push({ type: TOKEN.ITALIC, text: m[4] });
|
|
59
|
-
} else if (m[5]) {
|
|
60
|
-
// inline code
|
|
61
|
-
tokens.push({ type: TOKEN.CODE, text: m[6] });
|
|
62
|
-
} else if (m[7]) {
|
|
63
|
-
// link
|
|
64
|
-
tokens.push({ type: TOKEN.LINK, text: m[8], href: m[9] });
|
|
65
|
-
} else if (m[10]) {
|
|
66
|
-
// event_ref
|
|
67
|
-
tokens.push({
|
|
68
|
-
type: TOKEN.EVENT_REF,
|
|
69
|
-
eventId: m[11],
|
|
70
|
-
label: m[12] || m[11],
|
|
71
|
-
});
|
|
72
|
-
} else if (m[13]) {
|
|
73
|
-
// cite_ref
|
|
74
|
-
tokens.push({
|
|
75
|
-
type: TOKEN.CITE_REF,
|
|
76
|
-
blockId: m[14],
|
|
77
|
-
label: m[15] || m[14],
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
last = INLINE_RE.lastIndex;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Remaining plain text
|
|
85
|
-
if (last < text.length) {
|
|
86
|
-
tokens.push({ type: TOKEN.TEXT, text: text.slice(last) });
|
|
82
|
+
if (!matched) pos++;
|
|
87
83
|
}
|
|
88
84
|
|
|
85
|
+
if (textStart < text.length) tokens.push({ type: TOKEN.TEXT, text: text.slice(textStart) });
|
|
89
86
|
return tokens.length ? tokens : [{ type: TOKEN.TEXT, text }];
|
|
90
|
-
}
|
|
87
|
+
}
|
|
@@ -2,116 +2,133 @@
|
|
|
2
2
|
// Renders inline token arrays to either:
|
|
3
3
|
// a) A DocumentFragment (DOM nodes) — used by Viewer and Editor live preview
|
|
4
4
|
// b) An HTML string — used for SSR / export
|
|
5
|
-
//
|
|
6
|
-
// Callbacks:
|
|
7
|
-
// onEventClick(eventId) — called when an [[event:]] chip is clicked
|
|
8
|
-
// onCiteClick(blockId) — called when a [[cite:]] chip is clicked
|
|
9
5
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
6
|
|
|
11
7
|
import { tokenizeInline, TOKEN } from './InlineLexer.js';
|
|
12
8
|
|
|
13
|
-
//
|
|
9
|
+
// ── DOM rendering ─────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
14
11
|
export function renderInlineToDOM(text = '', { onEventClick, onCiteClick } = {}) {
|
|
15
12
|
const frag = document.createDocumentFragment();
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
switch (t.type) {
|
|
22
|
-
case TOKEN.TEXT:
|
|
23
|
-
node = document.createTextNode(t.text);
|
|
24
|
-
break;
|
|
25
|
-
|
|
26
|
-
case TOKEN.BOLD:
|
|
27
|
-
node = document.createElement('strong');
|
|
28
|
-
node.textContent = t.text;
|
|
29
|
-
break;
|
|
30
|
-
|
|
31
|
-
case TOKEN.ITALIC:
|
|
32
|
-
node = document.createElement('em');
|
|
33
|
-
node.textContent = t.text;
|
|
34
|
-
break;
|
|
35
|
-
|
|
36
|
-
case TOKEN.CODE: {
|
|
37
|
-
node = document.createElement('code');
|
|
38
|
-
node.className = 'griot-inline-code';
|
|
39
|
-
node.textContent = t.text;
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
case TOKEN.LINK: {
|
|
44
|
-
node = document.createElement('a');
|
|
45
|
-
node.href = t.href;
|
|
46
|
-
node.target = '_blank';
|
|
47
|
-
node.rel = 'noopener noreferrer';
|
|
48
|
-
node.className = 'griot-link';
|
|
49
|
-
node.textContent = t.text;
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
13
|
+
for (const t of tokenizeInline(text)) {
|
|
14
|
+
frag.appendChild(_toNode(t, { onEventClick, onCiteClick }));
|
|
15
|
+
}
|
|
16
|
+
return frag;
|
|
17
|
+
}
|
|
52
18
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
node.type = 'button';
|
|
56
|
-
node.className = 'griot-chip griot-chip--event';
|
|
57
|
-
node.dataset.eventId = t.eventId;
|
|
58
|
-
node.innerHTML = `<span class="griot-chip__icon">⏱</span><span class="griot-chip__label">${escHtml(t.label)}</span>`;
|
|
59
|
-
if (onEventClick) {
|
|
60
|
-
node.addEventListener('click', (e) => {
|
|
61
|
-
e.stopPropagation();
|
|
62
|
-
onEventClick(t.eventId);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
19
|
+
function _toNode(t, opts) {
|
|
20
|
+
switch (t.type) {
|
|
67
21
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
node.type = 'button';
|
|
71
|
-
node.className = 'griot-chip griot-chip--cite';
|
|
72
|
-
node.dataset.blockId = t.blockId;
|
|
73
|
-
node.innerHTML = `<span class="griot-chip__icon">📖</span><span class="griot-chip__label">${escHtml(t.label)}</span>`;
|
|
74
|
-
if (onCiteClick) {
|
|
75
|
-
node.addEventListener('click', (e) => {
|
|
76
|
-
e.stopPropagation();
|
|
77
|
-
onCiteClick(t.blockId);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
22
|
+
case TOKEN.TEXT:
|
|
23
|
+
return document.createTextNode(t.text);
|
|
82
24
|
|
|
83
|
-
|
|
84
|
-
|
|
25
|
+
case TOKEN.BOLD: {
|
|
26
|
+
const el = document.createElement('strong');
|
|
27
|
+
el.textContent = t.text;
|
|
28
|
+
return el;
|
|
29
|
+
}
|
|
30
|
+
case TOKEN.ITALIC: {
|
|
31
|
+
const el = document.createElement('em');
|
|
32
|
+
el.textContent = t.text;
|
|
33
|
+
return el;
|
|
34
|
+
}
|
|
35
|
+
case TOKEN.UNDERLINE: {
|
|
36
|
+
const el = document.createElement('u');
|
|
37
|
+
el.className = 'griot-underline';
|
|
38
|
+
el.textContent = t.text;
|
|
39
|
+
return el;
|
|
40
|
+
}
|
|
41
|
+
case TOKEN.STRIKE: {
|
|
42
|
+
const el = document.createElement('s');
|
|
43
|
+
el.className = 'griot-strike';
|
|
44
|
+
el.textContent = t.text;
|
|
45
|
+
return el;
|
|
46
|
+
}
|
|
47
|
+
case TOKEN.HIGHLIGHT: {
|
|
48
|
+
const el = document.createElement('mark');
|
|
49
|
+
el.className = 'griot-highlight';
|
|
50
|
+
el.textContent = t.text;
|
|
51
|
+
return el;
|
|
52
|
+
}
|
|
53
|
+
case TOKEN.COLOR_MARK: {
|
|
54
|
+
const el = document.createElement('span');
|
|
55
|
+
el.className = 'griot-color-mark';
|
|
56
|
+
el.style.color = t.color;
|
|
57
|
+
el.textContent = t.text;
|
|
58
|
+
return el;
|
|
59
|
+
}
|
|
60
|
+
case TOKEN.CODE: {
|
|
61
|
+
const el = document.createElement('code');
|
|
62
|
+
el.className = 'griot-inline-code';
|
|
63
|
+
el.textContent = t.code;
|
|
64
|
+
return el;
|
|
65
|
+
}
|
|
66
|
+
case TOKEN.IMAGE: {
|
|
67
|
+
const el = document.createElement('img');
|
|
68
|
+
el.src = t.src;
|
|
69
|
+
el.alt = t.alt ?? '';
|
|
70
|
+
el.className = 'griot-inline-img';
|
|
71
|
+
return el;
|
|
72
|
+
}
|
|
73
|
+
case TOKEN.LINK: {
|
|
74
|
+
const el = document.createElement('a');
|
|
75
|
+
el.href = t.href;
|
|
76
|
+
el.target = '_blank';
|
|
77
|
+
el.rel = 'noopener noreferrer';
|
|
78
|
+
el.className = 'griot-link';
|
|
79
|
+
el.textContent = t.label;
|
|
80
|
+
return el;
|
|
81
|
+
}
|
|
82
|
+
case TOKEN.EVENT_REF: {
|
|
83
|
+
const el = document.createElement('button');
|
|
84
|
+
el.type = 'button';
|
|
85
|
+
el.className = 'griot-chip griot-chip--event';
|
|
86
|
+
el.dataset.eventId = t.eventId;
|
|
87
|
+
el.innerHTML = `<span class="griot-chip__icon">⏱</span><span class="griot-chip__label">${escHtml(t.label)}</span>`;
|
|
88
|
+
if (opts.onEventClick) el.addEventListener('click', (e) => { e.stopPropagation(); opts.onEventClick(t.eventId); });
|
|
89
|
+
return el;
|
|
90
|
+
}
|
|
91
|
+
case TOKEN.CITE_REF: {
|
|
92
|
+
const el = document.createElement('button');
|
|
93
|
+
el.type = 'button';
|
|
94
|
+
el.className = 'griot-chip griot-chip--cite';
|
|
95
|
+
el.dataset.blockId = t.blockId;
|
|
96
|
+
el.innerHTML = `<span class="griot-chip__icon">📖</span><span class="griot-chip__label">${escHtml(t.label)}</span>`;
|
|
97
|
+
if (opts.onCiteClick) el.addEventListener('click', (e) => { e.stopPropagation(); opts.onCiteClick(t.blockId); });
|
|
98
|
+
return el;
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
default:
|
|
102
|
+
return document.createTextNode(t.text ?? '');
|
|
88
103
|
}
|
|
89
|
-
|
|
90
|
-
return frag;
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
//
|
|
106
|
+
// ── HTML string rendering ─────────────────────────────────────────────────────
|
|
107
|
+
|
|
94
108
|
export function renderInlineToHTML(text = '') {
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
return tokenizeInline(text).map(_toHTML).join('');
|
|
110
|
+
}
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
112
|
+
function _toHTML(t) {
|
|
113
|
+
switch (t.type) {
|
|
114
|
+
case TOKEN.TEXT: return escHtml(t.text);
|
|
115
|
+
case TOKEN.BOLD: return `<strong>${escHtml(t.text)}</strong>`;
|
|
116
|
+
case TOKEN.ITALIC: return `<em>${escHtml(t.text)}</em>`;
|
|
117
|
+
case TOKEN.UNDERLINE: return `<u class="griot-underline">${escHtml(t.text)}</u>`;
|
|
118
|
+
case TOKEN.STRIKE: return `<s class="griot-strike">${escHtml(t.text)}</s>`;
|
|
119
|
+
case TOKEN.HIGHLIGHT: return `<mark class="griot-highlight">${escHtml(t.text)}</mark>`;
|
|
120
|
+
case TOKEN.COLOR_MARK: return `<span class="griot-color-mark" style="color:${escAttr(t.color)}">${escHtml(t.text)}</span>`;
|
|
121
|
+
case TOKEN.CODE: return `<code class="griot-inline-code">${escHtml(t.code)}</code>`;
|
|
122
|
+
case TOKEN.IMAGE: return `<img class="griot-inline-img" src="${escAttr(t.src)}" alt="${escAttr(t.alt ?? '')}">`;
|
|
123
|
+
case TOKEN.LINK: return `<a class="griot-link" href="${escAttr(t.href)}" target="_blank" rel="noopener noreferrer">${escHtml(t.label)}</a>`;
|
|
124
|
+
case TOKEN.EVENT_REF: return `<button type="button" class="griot-chip griot-chip--event" data-event-id="${escAttr(t.eventId)}"><span class="griot-chip__icon">⏱</span><span class="griot-chip__label">${escHtml(t.label)}</span></button>`;
|
|
125
|
+
case TOKEN.CITE_REF: return `<button type="button" class="griot-chip griot-chip--cite" data-block-id="${escAttr(t.blockId)}"><span class="griot-chip__icon">📖</span><span class="griot-chip__label">${escHtml(t.label)}</span></button>`;
|
|
126
|
+
default: return escHtml(t.text ?? '');
|
|
109
127
|
}
|
|
110
|
-
|
|
111
|
-
return html;
|
|
112
128
|
}
|
|
113
129
|
|
|
114
|
-
//
|
|
130
|
+
// ── Escape helpers ────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
115
132
|
export function escHtml(s) {
|
|
116
133
|
return String(s ?? '')
|
|
117
134
|
.replace(/&/g, '&')
|
|
@@ -122,7 +139,5 @@ export function escHtml(s) {
|
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
export function escAttr(s) {
|
|
125
|
-
return String(s ?? '')
|
|
126
|
-
|
|
127
|
-
.replace(/"/g, '"');
|
|
128
|
-
}
|
|
142
|
+
return String(s ?? '').replace(/&/g, '&').replace(/"/g, '"');
|
|
143
|
+
}
|