@pugi/cli 0.1.0-alpha.9 → 0.1.0-beta.10
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 +33 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +16 -0
- package/dist/commands/deploy.js +439 -0
- package/dist/core/agents/loader.js +104 -0
- package/dist/core/agents/registry.js +1 -1
- package/dist/core/consensus/anvil-fanout.js +276 -0
- package/dist/core/consensus/diff-capture.js +382 -0
- package/dist/core/consensus/rubric.js +233 -0
- package/dist/core/context/index.js +21 -0
- package/dist/core/context/pugiignore.js +316 -0
- package/dist/core/context/repo-skeleton.js +533 -0
- package/dist/core/context/watcher.js +342 -0
- package/dist/core/context/working-set.js +165 -0
- package/dist/core/edits/dispatch.js +185 -0
- package/dist/core/edits/index.js +15 -0
- package/dist/core/edits/layer-a-apply.js +217 -0
- package/dist/core/edits/layer-b-apply.js +211 -0
- package/dist/core/edits/layer-c-apply.js +160 -0
- package/dist/core/edits/layer-d-ast.js +29 -0
- package/dist/core/edits/marker-parser.js +401 -0
- package/dist/core/edits/security-gate.js +223 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/native-pugi.js +6 -1
- package/dist/core/engine/prompts.js +8 -0
- package/dist/core/engine/tool-bridge.js +33 -1
- package/dist/core/lsp/client.js +719 -0
- package/dist/core/repl/ask.js +512 -0
- package/dist/core/repl/cancellation.js +98 -0
- package/dist/core/repl/dispatch-fsm.js +220 -0
- package/dist/core/repl/privacy-banner.js +71 -0
- package/dist/core/repl/session.js +1908 -13
- package/dist/core/repl/slash-commands.js +92 -32
- package/dist/core/repl/store/index.js +12 -0
- package/dist/core/repl/store/jsonl-log.js +321 -0
- package/dist/core/repl/store/lockfile.js +155 -0
- package/dist/core/repl/store/session-store.js +792 -0
- package/dist/core/repl/store/types.js +44 -0
- package/dist/core/repl/store/uuid-v7.js +68 -0
- package/dist/core/repl/workspace-context.js +72 -1
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/skills/loader.js +454 -0
- package/dist/core/skills/sources.js +480 -0
- package/dist/core/skills/trust.js +172 -0
- package/dist/runtime/cli.js +998 -12
- package/dist/runtime/commands/agents.js +385 -0
- package/dist/runtime/commands/config.js +338 -8
- package/dist/runtime/commands/delegate.js +289 -0
- package/dist/runtime/commands/lsp.js +206 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/review-consensus.js +399 -0
- package/dist/runtime/commands/roster.js +117 -0
- package/dist/runtime/commands/skills.js +401 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/tools/apply-patch.js +495 -0
- package/dist/tools/file-tools.js +90 -0
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/web-fetch.js +1 -1
- package/dist/tui/agent-tree-pane.js +9 -0
- package/dist/tui/ask-cli.js +52 -0
- package/dist/tui/ask-modal.js +211 -0
- package/dist/tui/conversation-pane.js +48 -3
- package/dist/tui/input-box.js +48 -5
- package/dist/tui/markdown-render.js +266 -0
- package/dist/tui/repl-render.js +319 -3
- package/dist/tui/repl-splash-mascot.js +130 -0
- package/dist/tui/repl-splash.js +7 -1
- package/dist/tui/repl.js +96 -12
- package/dist/tui/status-bar.js +63 -3
- package/dist/tui/tool-stream-pane.js +91 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +14 -6
package/dist/tui/input-box.js
CHANGED
|
@@ -131,14 +131,57 @@ export function InputBox(props) {
|
|
|
131
131
|
useInput((input, key) => {
|
|
132
132
|
if (key.ctrl && input === 'c') {
|
|
133
133
|
const t = now();
|
|
134
|
-
|
|
134
|
+
// α6.9: Claude Code-style double-press semantics. First Ctrl+C
|
|
135
|
+
// ALWAYS attempts to cancel an in-flight dispatch (when the
|
|
136
|
+
// session reports non-idle); second Ctrl+C within 1s exits the
|
|
137
|
+
// process. If onCancel is omitted (legacy callers, tests), the
|
|
138
|
+
// old behaviour is preserved: first Ctrl+C clears the buffer +
|
|
139
|
+
// arms the exit timer, second Ctrl+C exits.
|
|
140
|
+
const withinDoubleTapWindow = typeof lastCtrlCAt === 'number' && t - lastCtrlCAt <= CTRL_C_DOUBLE_TAP_MS;
|
|
141
|
+
if (withinDoubleTapWindow) {
|
|
142
|
+
// Second press inside the window — always exit. This matches
|
|
143
|
+
// Claude Code: even mid-dispatch, the second Ctrl+C wins so
|
|
144
|
+
// the operator can always escape a stuck REPL.
|
|
135
145
|
props.onExit();
|
|
136
146
|
return;
|
|
137
147
|
}
|
|
148
|
+
// First press in a fresh window. If the host wired a cancel
|
|
149
|
+
// surface and there is something to cancel, abort the dispatch.
|
|
150
|
+
// The buffer is left untouched on a cancel (the operator's
|
|
151
|
+
// current input is NOT trashed by an accidental Ctrl+C while a
|
|
152
|
+
// tool is running).
|
|
153
|
+
//
|
|
154
|
+
// Three-valued onCancel return (see prop docstring):
|
|
155
|
+
// - true → dispatch cancelled, keep buffer, arm exit timer
|
|
156
|
+
// - false → idle, clear buffer (legacy), arm exit timer
|
|
157
|
+
// - undefined → handler bypassed (modal owns input); NO state
|
|
158
|
+
// change at all. Buffer stays, exit timer NOT
|
|
159
|
+
// armed (otherwise the modal would silently
|
|
160
|
+
// promote a Ctrl+C to "press again to exit",
|
|
161
|
+
// which is wrong context for a modal cancel).
|
|
162
|
+
let cancelResult;
|
|
163
|
+
if (props.onCancel) {
|
|
164
|
+
cancelResult = props.onCancel();
|
|
165
|
+
}
|
|
166
|
+
if (cancelResult === undefined && props.onCancel) {
|
|
167
|
+
// Bypass path - modal owns the input. Drop the press silently
|
|
168
|
+
// so the modal's own cancel surface (Esc / its own Ctrl+C
|
|
169
|
+
// binding inside the modal component) takes effect on its own
|
|
170
|
+
// terms. P2 fix: previously this fell through to the
|
|
171
|
+
// legacy buffer-clear + setLastCtrlCAt path and wiped modal
|
|
172
|
+
// draft text on first Ctrl+C.
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
138
175
|
setLastCtrlCAt(t);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
176
|
+
// Legacy behaviour: on idle (or no onCancel wired), clear the
|
|
177
|
+
// buffer + reset search so the operator's screen is calm before
|
|
178
|
+
// they confirm exit. When we DID cancel a live dispatch, keep
|
|
179
|
+
// the buffer so a half-typed brief is not lost.
|
|
180
|
+
if (cancelResult !== true) {
|
|
181
|
+
setLine('');
|
|
182
|
+
setCursor(0);
|
|
183
|
+
setSearch(undefined);
|
|
184
|
+
}
|
|
142
185
|
return;
|
|
143
186
|
}
|
|
144
187
|
// Search-mode key handling. Ctrl+R / Ctrl+S cycle, Enter accepts,
|
|
@@ -456,7 +499,7 @@ export function InputBox(props) {
|
|
|
456
499
|
: Math.min(paletteIndex, paletteView.rows.length - 1);
|
|
457
500
|
const divider = '─'.repeat(innerWidth);
|
|
458
501
|
const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
|
|
459
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "cyan", dimColor: true, children: divider }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C ×2 exit' }) })] }));
|
|
502
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "cyan", dimColor: true, children: divider }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
|
|
460
503
|
}
|
|
461
504
|
/**
|
|
462
505
|
* Render the line with the cursor glyph inserted at `cursor`. The cursor
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const HEADING_COLORS = ['cyan', 'magenta', 'yellow'];
|
|
4
|
+
/**
|
|
5
|
+
* Very small keyword table for cosmetic code accent. The fence label is
|
|
6
|
+
* matched case-insensitively against a handful of languages we expect
|
|
7
|
+
* the personas to emit (ts/tsx/js/jsx/py/sh). Unknown languages render
|
|
8
|
+
* the body in white - no syntax error, no warning. Tests assert that
|
|
9
|
+
* an empty fence label still produces a bordered Box (no crash).
|
|
10
|
+
*/
|
|
11
|
+
const KEYWORDS_BY_LANG = {
|
|
12
|
+
ts: ['const', 'let', 'var', 'function', 'class', 'interface', 'type', 'import', 'export', 'from', 'return', 'if', 'else', 'for', 'while', 'await', 'async', 'new', 'this', 'static', 'public', 'private', 'protected', 'readonly', 'extends', 'implements'],
|
|
13
|
+
tsx: ['const', 'let', 'var', 'function', 'class', 'interface', 'type', 'import', 'export', 'from', 'return', 'if', 'else', 'for', 'while', 'await', 'async', 'new', 'this', 'static', 'public', 'private', 'protected', 'readonly', 'extends', 'implements'],
|
|
14
|
+
js: ['const', 'let', 'var', 'function', 'class', 'import', 'export', 'from', 'return', 'if', 'else', 'for', 'while', 'await', 'async', 'new', 'this'],
|
|
15
|
+
jsx: ['const', 'let', 'var', 'function', 'class', 'import', 'export', 'from', 'return', 'if', 'else', 'for', 'while', 'await', 'async', 'new', 'this'],
|
|
16
|
+
py: ['def', 'class', 'import', 'from', 'return', 'if', 'else', 'elif', 'for', 'while', 'with', 'as', 'try', 'except', 'finally', 'raise', 'lambda', 'pass', 'yield', 'None', 'True', 'False'],
|
|
17
|
+
sh: ['if', 'then', 'else', 'fi', 'for', 'do', 'done', 'while', 'case', 'esac', 'function', 'return', 'export'],
|
|
18
|
+
bash: ['if', 'then', 'else', 'fi', 'for', 'do', 'done', 'while', 'case', 'esac', 'function', 'return', 'export'],
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Top-level entry point. Splits the source into lines, runs the
|
|
22
|
+
* block-level state machine, and emits an Ink tree. The component is
|
|
23
|
+
* memo-friendly (pure function of `source`) but we don't wrap in
|
|
24
|
+
* React.memo here - the conversation pane already memoises row keys.
|
|
25
|
+
*/
|
|
26
|
+
export function MarkdownRender(props) {
|
|
27
|
+
const blocks = parseBlocks(props.source);
|
|
28
|
+
const children = blocks.map((block, index) => renderBlock(block, index));
|
|
29
|
+
if (props.inline) {
|
|
30
|
+
return _jsx(_Fragment, { children: children });
|
|
31
|
+
}
|
|
32
|
+
return _jsx(Box, { flexDirection: "column", children: children });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Walk the source line by line and emit one Block per visual unit. The
|
|
36
|
+
* state machine has two modes: default (heading / list / paragraph) and
|
|
37
|
+
* in-code (every line is appended verbatim to the current code block).
|
|
38
|
+
*
|
|
39
|
+
* Inside a code fence we DO NOT process inline syntax - the body is
|
|
40
|
+
* preserved literally so the operator sees code that actually runs.
|
|
41
|
+
*/
|
|
42
|
+
function parseBlocks(source) {
|
|
43
|
+
const lines = source.split(/\r?\n/);
|
|
44
|
+
const blocks = [];
|
|
45
|
+
let inCode = false;
|
|
46
|
+
let codeLang = '';
|
|
47
|
+
let codeBody = [];
|
|
48
|
+
for (const raw of lines) {
|
|
49
|
+
const line = raw;
|
|
50
|
+
if (inCode) {
|
|
51
|
+
if (/^```/.test(line)) {
|
|
52
|
+
blocks.push({ kind: 'code', lang: codeLang, body: codeBody.join('\n') });
|
|
53
|
+
inCode = false;
|
|
54
|
+
codeBody = [];
|
|
55
|
+
codeLang = '';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
codeBody.push(line);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const fenceMatch = /^```([A-Za-z0-9_+-]*)\s*$/.exec(line);
|
|
62
|
+
if (fenceMatch) {
|
|
63
|
+
inCode = true;
|
|
64
|
+
codeLang = (fenceMatch[1] ?? '').toLowerCase();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (line.trim().length === 0) {
|
|
68
|
+
blocks.push({ kind: 'blank' });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const headingMatch = /^(#{1,3})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
72
|
+
if (headingMatch) {
|
|
73
|
+
const level = headingMatch[1].length;
|
|
74
|
+
const text = headingMatch[2];
|
|
75
|
+
blocks.push({ kind: 'heading', level, text });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const bulletMatch = /^\s*[-*]\s+(.+)$/.exec(line);
|
|
79
|
+
if (bulletMatch) {
|
|
80
|
+
blocks.push({ kind: 'bullet', text: bulletMatch[1] });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const orderedMatch = /^\s*(\d+)\.\s+(.+)$/.exec(line);
|
|
84
|
+
if (orderedMatch) {
|
|
85
|
+
blocks.push({ kind: 'ordered', index: orderedMatch[1], text: orderedMatch[2] });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
blocks.push({ kind: 'paragraph', text: line });
|
|
89
|
+
}
|
|
90
|
+
// Unterminated fence: surface what we have as code so the operator
|
|
91
|
+
// still sees the body. Mirrors GitHub's render behavior.
|
|
92
|
+
if (inCode) {
|
|
93
|
+
blocks.push({ kind: 'code', lang: codeLang, body: codeBody.join('\n') });
|
|
94
|
+
}
|
|
95
|
+
return blocks;
|
|
96
|
+
}
|
|
97
|
+
/* ------------------------------------------------------------------ */
|
|
98
|
+
/* Block renderers */
|
|
99
|
+
/* ------------------------------------------------------------------ */
|
|
100
|
+
function renderBlock(block, key) {
|
|
101
|
+
switch (block.kind) {
|
|
102
|
+
case 'heading': {
|
|
103
|
+
const color = HEADING_COLORS[block.level - 1] ?? 'cyan';
|
|
104
|
+
const prefix = '#'.repeat(block.level);
|
|
105
|
+
return (_jsx(Text, { bold: true, color: color, children: `${prefix} ${block.text}` }, key));
|
|
106
|
+
}
|
|
107
|
+
case 'paragraph':
|
|
108
|
+
return (_jsx(Text, { children: renderInline(block.text) }, key));
|
|
109
|
+
case 'bullet':
|
|
110
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '• ' }), _jsx(Text, { children: renderInline(block.text) })] }, key));
|
|
111
|
+
case 'ordered':
|
|
112
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: `${block.index}. ` }), _jsx(Text, { children: renderInline(block.text) })] }, key));
|
|
113
|
+
case 'code':
|
|
114
|
+
return renderCodeBlock(block.lang, block.body, key);
|
|
115
|
+
case 'blank':
|
|
116
|
+
// One-line spacer between blocks. We render an empty Text so the
|
|
117
|
+
// height accounting in Ink matches what the operator sees.
|
|
118
|
+
return _jsx(Text, { children: " " }, key);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function renderCodeBlock(lang, body, key) {
|
|
122
|
+
const lines = body.split('\n');
|
|
123
|
+
const keywords = KEYWORDS_BY_LANG[lang] ?? [];
|
|
124
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: lines.map((line, index) => (_jsx(Text, { children: renderCodeLine(line, keywords) }, index))) }, key));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Per-line code accent. We do NOT build a real lexer - we split on
|
|
128
|
+
* whitespace + punctuation boundaries and color matched keywords cyan,
|
|
129
|
+
* single/double quoted strings green, and `//` / `#` comment tails gray.
|
|
130
|
+
* The output is a list of Ink Text spans. Anything unmatched stays
|
|
131
|
+
* plain white.
|
|
132
|
+
*/
|
|
133
|
+
function renderCodeLine(line, keywords) {
|
|
134
|
+
// Comment tail wins first - we strip it and color the rest.
|
|
135
|
+
const commentIndex = findCommentStart(line);
|
|
136
|
+
const code = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
|
|
137
|
+
const comment = commentIndex >= 0 ? line.slice(commentIndex) : '';
|
|
138
|
+
const spans = [];
|
|
139
|
+
// Tokenise the code portion: alternate runs of word chars vs the rest.
|
|
140
|
+
// Strings are matched as a whole quoted span. The regex is anchored
|
|
141
|
+
// sticky to avoid catastrophic backtracking on long lines.
|
|
142
|
+
const tokenRe = /("[^"]*"|'[^']*'|`[^`]*`|\w+|\s+|[^\s\w]+)/g;
|
|
143
|
+
let match;
|
|
144
|
+
let key = 0;
|
|
145
|
+
while ((match = tokenRe.exec(code)) !== null) {
|
|
146
|
+
const tok = match[0];
|
|
147
|
+
if (/^["'`].*["'`]$/.test(tok)) {
|
|
148
|
+
spans.push(_jsx(Text, { color: "green", children: tok }, key));
|
|
149
|
+
}
|
|
150
|
+
else if (keywords.includes(tok)) {
|
|
151
|
+
spans.push(_jsx(Text, { color: "cyan", bold: true, children: tok }, key));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
spans.push(_jsx(Text, { children: tok }, key));
|
|
155
|
+
}
|
|
156
|
+
key += 1;
|
|
157
|
+
}
|
|
158
|
+
if (comment.length > 0) {
|
|
159
|
+
spans.push(_jsx(Text, { color: "gray", children: comment }, "comment"));
|
|
160
|
+
}
|
|
161
|
+
return _jsx(_Fragment, { children: spans });
|
|
162
|
+
}
|
|
163
|
+
function findCommentStart(line) {
|
|
164
|
+
// Single-line // or # outside of string literals. The probe is
|
|
165
|
+
// adequate for the persona-emitted snippets - a string literal that
|
|
166
|
+
// contains `//` would mis-color, but the output stays readable.
|
|
167
|
+
let inString = null;
|
|
168
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
169
|
+
const ch = line[i];
|
|
170
|
+
if (inString) {
|
|
171
|
+
if (ch === inString && line[i - 1] !== '\\')
|
|
172
|
+
inString = null;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
176
|
+
inString = ch;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === '/' && line[i + 1] === '/')
|
|
180
|
+
return i;
|
|
181
|
+
if (ch === '#')
|
|
182
|
+
return i;
|
|
183
|
+
}
|
|
184
|
+
return -1;
|
|
185
|
+
}
|
|
186
|
+
/* ------------------------------------------------------------------ */
|
|
187
|
+
/* Inline tokeniser */
|
|
188
|
+
/* ------------------------------------------------------------------ */
|
|
189
|
+
/**
|
|
190
|
+
* Parse one paragraph line into a list of inline spans, then render
|
|
191
|
+
* them as Ink Text nodes. We walk the source left-to-right and greedily
|
|
192
|
+
* consume the longest delimiter we find. Unmatched delimiters fall
|
|
193
|
+
* through as literal text - personas type `**` mid-sentence sometimes.
|
|
194
|
+
*/
|
|
195
|
+
function renderInline(source) {
|
|
196
|
+
const spans = tokeniseInline(source);
|
|
197
|
+
return (_jsx(_Fragment, { children: spans.map((span, index) => renderSpan(span, index)) }));
|
|
198
|
+
}
|
|
199
|
+
function tokeniseInline(source) {
|
|
200
|
+
const spans = [];
|
|
201
|
+
let buffer = '';
|
|
202
|
+
let i = 0;
|
|
203
|
+
while (i < source.length) {
|
|
204
|
+
const rest = source.slice(i);
|
|
205
|
+
// Inline code wins over bold/italic so that `**` inside backticks
|
|
206
|
+
// renders literally.
|
|
207
|
+
const codeMatch = /^`([^`]+)`/.exec(rest);
|
|
208
|
+
if (codeMatch) {
|
|
209
|
+
flush(buffer, spans);
|
|
210
|
+
buffer = '';
|
|
211
|
+
spans.push({ kind: 'code', text: codeMatch[1] });
|
|
212
|
+
i += codeMatch[0].length;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Link before bold/italic so `[text **bold**](url)` renders linkish.
|
|
216
|
+
const linkMatch = /^\[([^\]]+)\]\(([^)\s]+)\)/.exec(rest);
|
|
217
|
+
if (linkMatch) {
|
|
218
|
+
flush(buffer, spans);
|
|
219
|
+
buffer = '';
|
|
220
|
+
spans.push({ kind: 'link', text: linkMatch[1], url: linkMatch[2] });
|
|
221
|
+
i += linkMatch[0].length;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Bold (**) before italic (*) so the greedy match wins.
|
|
225
|
+
const boldMatch = /^\*\*([^*]+)\*\*/.exec(rest);
|
|
226
|
+
if (boldMatch) {
|
|
227
|
+
flush(buffer, spans);
|
|
228
|
+
buffer = '';
|
|
229
|
+
spans.push({ kind: 'bold', text: boldMatch[1] });
|
|
230
|
+
i += boldMatch[0].length;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const italicMatch = /^\*([^*]+)\*/.exec(rest);
|
|
234
|
+
if (italicMatch) {
|
|
235
|
+
flush(buffer, spans);
|
|
236
|
+
buffer = '';
|
|
237
|
+
spans.push({ kind: 'italic', text: italicMatch[1] });
|
|
238
|
+
i += italicMatch[0].length;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
buffer += source[i];
|
|
242
|
+
i += 1;
|
|
243
|
+
}
|
|
244
|
+
flush(buffer, spans);
|
|
245
|
+
return spans;
|
|
246
|
+
}
|
|
247
|
+
function flush(buffer, spans) {
|
|
248
|
+
if (buffer.length === 0)
|
|
249
|
+
return;
|
|
250
|
+
spans.push({ kind: 'text', text: buffer });
|
|
251
|
+
}
|
|
252
|
+
function renderSpan(span, key) {
|
|
253
|
+
switch (span.kind) {
|
|
254
|
+
case 'text':
|
|
255
|
+
return _jsx(Text, { children: span.text }, key);
|
|
256
|
+
case 'bold':
|
|
257
|
+
return _jsx(Text, { bold: true, children: span.text }, key);
|
|
258
|
+
case 'italic':
|
|
259
|
+
return _jsx(Text, { italic: true, children: span.text }, key);
|
|
260
|
+
case 'code':
|
|
261
|
+
return _jsx(Text, { color: "green", children: span.text }, key);
|
|
262
|
+
case 'link':
|
|
263
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", underline: true, children: span.text }), _jsx(Text, { dimColor: true, children: ` (${span.url})` })] }, key));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=markdown-render.js.map
|