@0m0g1/griot 0.1.2 → 0.1.3

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.
@@ -0,0 +1,138 @@
1
+ // ─── FormatToolbar.js ─────────────────────────────────────────────────────────
2
+ // Floating toolbar that appears above a text selection inside the editor.
3
+ // Provides one-click inline formatting (bold, italic, underline, etc.) and
4
+ // link / color-mark insertion.
5
+ //
6
+ // Usage:
7
+ // const tb = new FormatToolbar(editorContainerEl, {
8
+ // onWrap(syntax) {}, // wrap selection with syntax chars e.g. '**'
9
+ // onLink() {}, // open link insertion prompt
10
+ // onColor() {}, // open color-mark prompt
11
+ // });
12
+ // tb.destroy();
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ const FORMATS = [
16
+ { key: 'bold', label: 'B', title: 'Bold (Ctrl+B)', syntax: '**' },
17
+ { key: 'italic', label: 'I', title: 'Italic (Ctrl+I)', syntax: '*' },
18
+ { key: 'underline', label: 'U', title: 'Underline (Ctrl+U)', syntax: '__' },
19
+ { key: 'strike', label: 'S̶', title: 'Strikethrough', syntax: '~~' },
20
+ { key: 'code', label: '`', title: 'Inline Code', syntax: '`' },
21
+ { key: 'highlight', label: '▐', title: 'Highlight', syntax: '==' },
22
+ { key: 'link', label: '🔗', title: 'Link', action: 'link' },
23
+ { key: 'color', label: '🎨', title: 'Color', action: 'color' },
24
+ ];
25
+
26
+ export class FormatToolbar {
27
+ constructor(container, callbacks = {}) {
28
+ this._container = container;
29
+ this._cb = callbacks;
30
+ this._el = null;
31
+ this._visible = false;
32
+
33
+ this._build();
34
+ this._attach();
35
+ }
36
+
37
+ // ── Build DOM ────────────────────────────────────────────────────────────────
38
+
39
+ _build() {
40
+ const el = document.createElement('div');
41
+ el.className = 'griot-format-toolbar';
42
+ el.setAttribute('role', 'toolbar');
43
+ el.setAttribute('aria-label', 'Inline formatting');
44
+
45
+ for (const fmt of FORMATS) {
46
+ const btn = document.createElement('button');
47
+ btn.type = 'button';
48
+ btn.className = `griot-format-toolbar__btn griot-ftb--${fmt.key}`;
49
+ btn.title = fmt.title;
50
+ btn.textContent = fmt.label;
51
+
52
+ btn.addEventListener('mousedown', (e) => {
53
+ e.preventDefault(); // preserve selection before acting
54
+ if (fmt.syntax) this._cb.onWrap?.(fmt.syntax);
55
+ else if (fmt.action === 'link') this._cb.onLink?.();
56
+ else if (fmt.action === 'color') this._cb.onColor?.();
57
+ this._hide();
58
+ });
59
+
60
+ el.appendChild(btn);
61
+ }
62
+
63
+ document.body.appendChild(el);
64
+ this._el = el;
65
+ }
66
+
67
+ // ── Event listeners ──────────────────────────────────────────────────────────
68
+
69
+ _attach() {
70
+ // Show after mouse release inside editor
71
+ this._onMouseUp = () => setTimeout(() => this._checkAndShow(), 20);
72
+ this._container.addEventListener('mouseup', this._onMouseUp);
73
+
74
+ // Show after Shift+arrow / Ctrl+A keyboard selection
75
+ this._onKeyUp = (e) => {
76
+ if (e.shiftKey || ((e.ctrlKey || e.metaKey) && e.key === 'a')) {
77
+ setTimeout(() => this._checkAndShow(), 20);
78
+ }
79
+ };
80
+ this._container.addEventListener('keyup', this._onKeyUp);
81
+
82
+ // Hide when clicking anywhere outside the toolbar
83
+ this._onDocDown = (e) => {
84
+ if (this._visible && !this._el.contains(e.target)) this._hide();
85
+ };
86
+ document.addEventListener('mousedown', this._onDocDown);
87
+ }
88
+
89
+ // ── Visibility ───────────────────────────────────────────────────────────────
90
+
91
+ _checkAndShow() {
92
+ const sel = window.getSelection();
93
+ if (!sel || sel.isCollapsed || !sel.rangeCount) { this._hide(); return; }
94
+
95
+ const range = sel.getRangeAt(0);
96
+ if (!this._container.contains(range.commonAncestorContainer)) { this._hide(); return; }
97
+ if (!range.toString().trim()) { this._hide(); return; }
98
+
99
+ this._show(range.getBoundingClientRect());
100
+ }
101
+
102
+ _show(rect) {
103
+ const el = this._el;
104
+ el.style.display = 'flex';
105
+
106
+ requestAnimationFrame(() => {
107
+ const tbW = el.offsetWidth || 280;
108
+ const tbH = el.offsetHeight || 36;
109
+
110
+ let left = rect.left + rect.width / 2 - tbW / 2 + window.scrollX;
111
+ let top = rect.top - tbH - 10 + window.scrollY;
112
+
113
+ // Clamp horizontally
114
+ left = Math.max(8 + window.scrollX, Math.min(left, window.scrollX + window.innerWidth - tbW - 8));
115
+ // Flip below if too close to top
116
+ if (top < window.scrollY + 8) top = rect.bottom + 8 + window.scrollY;
117
+
118
+ el.style.left = `${left}px`;
119
+ el.style.top = `${top}px`;
120
+ this._visible = true;
121
+ });
122
+ }
123
+
124
+ _hide() {
125
+ if (this._el) this._el.style.display = 'none';
126
+ this._visible = false;
127
+ }
128
+
129
+ // ── Lifecycle ────────────────────────────────────────────────────────────────
130
+
131
+ destroy() {
132
+ this._container.removeEventListener('mouseup', this._onMouseUp);
133
+ this._container.removeEventListener('keyup', this._onKeyUp);
134
+ document.removeEventListener('mousedown', this._onDocDown);
135
+ this._el?.remove();
136
+ this._el = null;
137
+ }
138
+ }
@@ -1,169 +1,155 @@
1
- // ─── Keyboard.js ──────────────────────────────────────────────────────────────
2
- // All keyboard behaviour for the block editor.
3
- // Pure logic receives the current doc + focused block ID, returns events
4
- // the Editor class acts on.
5
- //
6
- // Exported: attachKeyboardHandler(el, callbacks)
7
- // Attaches a keydown listener to a contenteditable element for one block.
8
- // Calls back into Editor which owns the document state.
1
+ // ─── Keyboard.js ─────────────────────────────────────────────────────────────
2
+ // Keyboard event handling for contenteditable editor blocks.
3
+ // Also exports cursor position helpers shared by Editor and FormatToolbar.
9
4
  // ─────────────────────────────────────────────────────────────────────────────
10
5
 
11
6
  /**
12
- * @param {HTMLElement} el The contenteditable element
13
- * @param {object} callbacks
14
- * onEnter(blockId, offset) — Enter pressed: split at cursor offset
15
- * onBackspaceAtStart(blockId) Backspace at offset 0: merge with prev
16
- * onDeleteAtEnd(blockId) Delete at end: merge next into this
17
- * onTab(blockId, shift) Tab / Shift+Tab
18
- * onArrowUp(blockId) Arrow up at first line → move focus to prev block
19
- * onArrowDown(blockId) Arrow down at last line → move focus to next block
20
- * onUndo() Ctrl/Cmd+Z
21
- * onRedo() — Ctrl/Cmd+Shift+Z or Ctrl+Y
7
+ * Attach all editor keyboard shortcuts to a contenteditable element.
8
+ *
9
+ * Callbacks:
10
+ * onEnter(id, offset) Enter (no shift)
11
+ * onBackspaceAtStart(id) Backspace at offset 0 with no selection
12
+ * onDeleteAtEnd(id) Delete at end with no selection
13
+ * onTab(id, isShift) Tab / Shift+Tab
14
+ * onArrowUp(id) on first visual line
15
+ * onArrowDown(id) ↓ on last visual line
16
+ * onUndo() — Ctrl/Cmd+Z
17
+ * onRedo() — Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z
18
+ * onFormatKey(key) — Ctrl/Cmd+B/I/U
22
19
  */
23
- export function attachKeyboardHandler(el, blockId, callbacks) {
24
- el.addEventListener('keydown', (e) => {
25
- const {
26
- onEnter, onBackspaceAtStart, onDeleteAtEnd,
27
- onTab, onArrowUp, onArrowDown, onUndo, onRedo,
28
- } = callbacks;
20
+ export function attachKeyboardHandler(el, blockId, callbacks = {}) {
21
+ const {
22
+ onEnter, onBackspaceAtStart, onDeleteAtEnd,
23
+ onTab, onArrowUp, onArrowDown,
24
+ onUndo, onRedo, onFormatKey,
25
+ } = callbacks;
29
26
 
27
+ el.addEventListener('keydown', (e) => {
30
28
  const ctrl = e.ctrlKey || e.metaKey;
31
29
 
32
- // ── Undo / Redo ──────────────────────────────────────────────
33
- if (ctrl && e.key === 'z' && !e.shiftKey) {
34
- e.preventDefault();
35
- onUndo?.();
36
- return;
37
- }
38
- if ((ctrl && e.key === 'z' && e.shiftKey) || (ctrl && e.key === 'y')) {
30
+ // ── Undo / Redo ───────────────────────────────────────────────────────────
31
+ if (ctrl && e.key === 'z' && !e.shiftKey) { e.preventDefault(); onUndo?.(); return; }
32
+ if (ctrl && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); onRedo?.(); return; }
33
+
34
+ // ── Inline format shortcuts ───────────────────────────────────────────────
35
+ if (ctrl && ['b', 'i', 'u'].includes(e.key.toLowerCase())) {
39
36
  e.preventDefault();
40
- onRedo?.();
37
+ onFormatKey?.(e.key.toLowerCase());
41
38
  return;
42
39
  }
43
40
 
44
- // ── Enter → split block ──────────────────────────────────────
41
+ // ── Enter ─────────────────────────────────────────────────────────────────
45
42
  if (e.key === 'Enter' && !e.shiftKey) {
46
43
  e.preventDefault();
47
44
  onEnter?.(blockId, getCursorOffset(el));
48
45
  return;
49
46
  }
50
47
 
51
- // ── Backspace at start → merge with previous ─────────────────
52
- if (e.key === 'Backspace' && getCursorOffset(el) === 0 && !hasSelection()) {
48
+ // ── Backspace at start ────────────────────────────────────────────────────
49
+ if (e.key === 'Backspace' && getCursorOffset(el) === 0 && selLen(el) === 0) {
53
50
  e.preventDefault();
54
51
  onBackspaceAtStart?.(blockId);
55
52
  return;
56
53
  }
57
54
 
58
- // ── Delete at end → merge next into this ─────────────────────
59
- if (e.key === 'Delete' && getCursorOffset(el) === el.textContent.length && !hasSelection()) {
55
+ // ── Delete at end ─────────────────────────────────────────────────────────
56
+ if (e.key === 'Delete' && isAtEnd(el) && selLen(el) === 0) {
60
57
  e.preventDefault();
61
58
  onDeleteAtEnd?.(blockId);
62
59
  return;
63
60
  }
64
61
 
65
- // ── Tab ───────────────────────────────────────────────────────
62
+ // ── Tab ───────────────────────────────────────────────────────────────────
66
63
  if (e.key === 'Tab') {
67
64
  e.preventDefault();
68
65
  onTab?.(blockId, e.shiftKey);
69
66
  return;
70
67
  }
71
68
 
72
- // ── Arrow navigation across blocks ───────────────────────────
73
- if (e.key === 'ArrowUp' && isAtFirstLine(el)) {
74
- e.preventDefault();
75
- onArrowUp?.(blockId);
76
- return;
77
- }
78
- if (e.key === 'ArrowDown' && isAtLastLine(el)) {
79
- e.preventDefault();
80
- onArrowDown?.(blockId);
81
- return;
82
- }
69
+ // ── Arrow navigation between blocks ──────────────────────────────────────
70
+ if (e.key === 'ArrowUp' && isOnFirstLine(el)) { e.preventDefault(); onArrowUp?.(blockId); }
71
+ if (e.key === 'ArrowDown' && isOnLastLine(el)) { e.preventDefault(); onArrowDown?.(blockId); }
83
72
  });
84
73
  }
85
74
 
86
- // ─── Cursor helpers ───────────────────────────────────────────────────────────
75
+ // ── Cursor offset helpers ─────────────────────────────────────────────────────
87
76
 
88
- /** Returns the character offset of the caret within el.textContent */
77
+ /** Character offset of the caret within `el`. */
89
78
  export function getCursorOffset(el) {
90
79
  const sel = window.getSelection();
91
- if (!sel || sel.rangeCount === 0) return 0;
92
- const range = sel.getRangeAt(0).cloneRange();
93
- range.selectNodeContents(el);
94
- range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
95
- return range.toString().length;
80
+ if (!sel?.rangeCount) return 0;
81
+ const r = sel.getRangeAt(0);
82
+ const pre = r.cloneRange();
83
+ pre.selectNodeContents(el);
84
+ pre.setEnd(r.startContainer, r.startOffset);
85
+ return pre.toString().length;
86
+ }
87
+
88
+ /** { start, end } character offsets of the current selection within `el`. */
89
+ export function getSelectionOffsets(el) {
90
+ const sel = window.getSelection();
91
+ if (!sel?.rangeCount) return { start: 0, end: 0 };
92
+ const r = sel.getRangeAt(0);
93
+ const pre = r.cloneRange();
94
+ pre.selectNodeContents(el);
95
+ pre.setEnd(r.startContainer, r.startOffset);
96
+ const start = pre.toString().length;
97
+ const end = start + r.toString().length;
98
+ return { start, end };
96
99
  }
97
100
 
98
- /** Place the caret at a specific character offset within el */
101
+ /** Move the caret to `offset` characters from the start of `el`. */
99
102
  export function setCursorOffset(el, offset) {
100
- const range = document.createRange();
101
103
  const sel = window.getSelection();
102
- if (!sel) return;
103
-
104
- let remaining = offset;
105
- let found = false;
104
+ const range = document.createRange();
105
+ let rem = offset, found = false;
106
106
 
107
- function walk(node) {
107
+ (function walk(node) {
108
108
  if (found) return;
109
109
  if (node.nodeType === Node.TEXT_NODE) {
110
- if (remaining <= node.textContent.length) {
111
- range.setStart(node, remaining);
112
- range.setEnd(node, remaining);
110
+ if (rem <= node.length) {
111
+ range.setStart(node, rem);
112
+ range.setEnd(node, rem);
113
113
  found = true;
114
114
  } else {
115
- remaining -= node.textContent.length;
115
+ rem -= node.length;
116
116
  }
117
117
  } else {
118
- for (const child of node.childNodes) walk(child);
118
+ node.childNodes.forEach(walk);
119
119
  }
120
- }
121
-
122
- walk(el);
123
-
124
- if (!found) {
125
- // Clamp to end
126
- range.selectNodeContents(el);
127
- range.collapse(false);
128
- }
120
+ })(el);
129
121
 
122
+ if (!found) { range.selectNodeContents(el); range.collapse(false); }
130
123
  sel.removeAllRanges();
131
124
  sel.addRange(range);
132
125
  }
133
126
 
134
- /** Focus el and place caret at end */
135
- export function focusAtEnd(el) {
136
- el.focus();
137
- setCursorOffset(el, el.textContent.length);
138
- }
127
+ export function focusAtEnd(el) { el.focus(); setCursorOffset(el, el.textContent?.length ?? 0); }
128
+ export function focusAtStart(el) { el.focus(); setCursorOffset(el, 0); }
139
129
 
140
- /** Focus el and place caret at start */
141
- export function focusAtStart(el) {
142
- el.focus();
143
- setCursorOffset(el, 0);
144
- }
130
+ // ── Internal helpers ──────────────────────────────────────────────────────────
145
131
 
146
- // ─── Line detection ───────────────────────────────────────────────────────────
147
- function hasSelection() {
148
- const sel = window.getSelection();
149
- return sel && sel.type === 'Range';
132
+ function selLen(el) {
133
+ try { return window.getSelection()?.getRangeAt(0)?.toString().length ?? 0; }
134
+ catch { return 0; }
150
135
  }
151
136
 
152
- function isAtFirstLine(el) {
153
- const sel = window.getSelection();
154
- if (!sel || sel.rangeCount === 0) return false;
155
- const range = sel.getRangeAt(0);
156
- const rect = range.getBoundingClientRect();
157
- const elRect = el.getBoundingClientRect();
158
- // Within 1.5x line-height from top
159
- return Math.abs(rect.top - elRect.top) < 30;
137
+ function isAtEnd(el) {
138
+ return getCursorOffset(el) >= (el.textContent?.length ?? 0);
160
139
  }
161
140
 
162
- function isAtLastLine(el) {
163
- const sel = window.getSelection();
164
- if (!sel || sel.rangeCount === 0) return false;
165
- const range = sel.getRangeAt(0);
166
- const rect = range.getBoundingClientRect();
167
- const elRect = el.getBoundingClientRect();
168
- return Math.abs(rect.bottom - elRect.bottom) < 30;
141
+ function isOnFirstLine(el) {
142
+ try {
143
+ const sel = window.getSelection();
144
+ if (!sel?.rangeCount) return true;
145
+ return sel.getRangeAt(0).getBoundingClientRect().top < el.getBoundingClientRect().top + 10;
146
+ } catch { return true; }
169
147
  }
148
+
149
+ function isOnLastLine(el) {
150
+ try {
151
+ const sel = window.getSelection();
152
+ if (!sel?.rangeCount) return true;
153
+ return sel.getRangeAt(0).getBoundingClientRect().bottom > el.getBoundingClientRect().bottom - 10;
154
+ } catch { return true; }
155
+ }
@@ -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
+ }