@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.
@@ -1,90 +1,87 @@
1
1
  // ─── InlineLexer.js ───────────────────────────────────────────────────────────
2
- // Tokenises inline markdown-like syntax into a flat token array.
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
- // **bold** → { type:'bold', text }
7
- // *italic* → { type:'italic', text }
8
- // `code` → { type:'code', text }
9
- // [label](url) → { type:'link', text, href }
10
- // [[event:id|label]] → { type:'event_ref', eventId, label }
11
- // [[cite:blockId|label]] → { type:'cite_ref', blockId, label }
12
- // plain text → { type:'text', text }
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
+ // ![alt](url) → 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: 'text',
17
- BOLD: 'bold',
18
- ITALIC: 'italic',
19
- CODE: 'code',
20
- LINK: 'link',
21
- EVENT_REF: 'event_ref',
22
- CITE_REF: 'cite_ref',
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
- // Combined regex — order matters (longest/most-specific first)
26
- const INLINE_RE = new RegExp(
27
- [
28
- /(\*\*(.+?)\*\*)/, // bold
29
- /(\*(.+?)\*)/, // italic
30
- /(`([^`]+)`)/, // inline code
31
- /(\[(.+?)\]\((.+?)\))/, // link
32
- /(\[\[event:([^\]|]+)(?:\|([^\]]*))?\]\])/, // event_ref
33
- /(\[\[cite:([^\]|]+)(?:\|([^\]]*))?\]\])/, // cite_ref
34
- ].map(r => r.source).join('|'),
35
- 'g'
36
- );
37
+ const RULES = [
38
+ // Inline image ![alt](url) — 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 last = 0;
43
- let m;
65
+ let pos = 0, textStart = 0;
44
66
 
45
- INLINE_RE.lastIndex = 0;
67
+ while (pos < text.length) {
68
+ const remaining = text.slice(pos);
69
+ let matched = false;
46
70
 
47
- while ((m = INLINE_RE.exec(text)) !== null) {
48
- // Flush plain text before this match
49
- if (m.index > last) {
50
- tokens.push({ type: TOKEN.TEXT, text: text.slice(last, m.index) });
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 (m[1]) {
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
- // ─── DOM rendering ────────────────────────────────────────────────────────────
9
+ // ── DOM rendering ─────────────────────────────────────────────────────────────
10
+
14
11
  export function renderInlineToDOM(text = '', { onEventClick, onCiteClick } = {}) {
15
12
  const frag = document.createDocumentFragment();
16
- const tokens = tokenizeInline(text);
17
-
18
- for (const t of tokens) {
19
- let node;
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
- case TOKEN.EVENT_REF: {
54
- node = document.createElement('button');
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
- case TOKEN.CITE_REF: {
69
- node = document.createElement('button');
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
- default:
84
- node = document.createTextNode(t.text ?? '');
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
- frag.appendChild(node);
101
+ default:
102
+ return document.createTextNode(t.text ?? '');
88
103
  }
89
-
90
- return frag;
91
104
  }
92
105
 
93
- // ─── HTML string rendering ────────────────────────────────────────────────────
106
+ // ── HTML string rendering ─────────────────────────────────────────────────────
107
+
94
108
  export function renderInlineToHTML(text = '') {
95
- const tokens = tokenizeInline(text);
96
- let html = '';
109
+ return tokenizeInline(text).map(_toHTML).join('');
110
+ }
97
111
 
98
- for (const t of tokens) {
99
- switch (t.type) {
100
- case TOKEN.TEXT: html += escHtml(t.text); break;
101
- case TOKEN.BOLD: html += `<strong>${escHtml(t.text)}</strong>`; break;
102
- case TOKEN.ITALIC: html += `<em>${escHtml(t.text)}</em>`; break;
103
- case TOKEN.CODE: html += `<code class="griot-inline-code">${escHtml(t.text)}</code>`; break;
104
- case TOKEN.LINK: html += `<a class="griot-link" href="${escAttr(t.href)}" target="_blank" rel="noopener noreferrer">${escHtml(t.text)}</a>`; break;
105
- case TOKEN.EVENT_REF: html += `<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>`; break;
106
- case TOKEN.CITE_REF: html += `<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>`; break;
107
- default: html += escHtml(t.text ?? '');
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
- // ─── Escape helpers ───────────────────────────────────────────────────────────
130
+ // ── Escape helpers ────────────────────────────────────────────────────────────
131
+
115
132
  export function escHtml(s) {
116
133
  return String(s ?? '')
117
134
  .replace(/&/g, '&amp;')
@@ -122,7 +139,5 @@ export function escHtml(s) {
122
139
  }
123
140
 
124
141
  export function escAttr(s) {
125
- return String(s ?? '')
126
- .replace(/&/g, '&amp;')
127
- .replace(/"/g, '&quot;');
128
- }
142
+ return String(s ?? '').replace(/&/g, '&amp;').replace(/"/g, '&quot;');
143
+ }