Initial Release: OpenQode Public Alpha v1.3

This commit is contained in:
Gemini AI
2025-12-14 00:40:14 +04:00
Unverified
commit 8e8d80c110
119 changed files with 31174 additions and 0 deletions

332
bin/ink-markdown-esm.mjs Normal file
View File

@@ -0,0 +1,332 @@
/**
* Block-Based Markdown Renderer for Ink
*
* CRITICAL FIX: This renderer ensures headings, paragraphs, and lists
* are NEVER merged into the same line. Each block is a separate Box.
*
* The previous bug: "## Initial Observationssome general thoughts"
* happened because inline rendering merged blocks.
*
* This renderer:
* 1. Parses markdown into AST using remark
* 2. Converts AST to block array
* 3. Renders each block as a separate Ink Box with spacing
*/
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import React from 'react';
import { Box, Text } from 'ink';
import Highlight from 'ink-syntax-highlight';
import he from 'he';
import { theme } from './tui-theme.mjs';
const h = React.createElement;
// ═══════════════════════════════════════════════════════════════
// BLOCK TYPES
// ═══════════════════════════════════════════════════════════════
/**
* Block types that get their own Box with spacing:
* - heading: #, ##, ###
* - paragraph: plain text blocks
* - code: fenced code blocks
* - list: ul/ol with items
* - quote: blockquotes
* - thematicBreak: horizontal rule
*/
// ═══════════════════════════════════════════════════════════════
// AST TO TEXT EXTRACTION (for inline content)
// ═══════════════════════════════════════════════════════════════
function extractText(node) {
if (!node) return '';
if (node.type === 'text') {
return he.decode(node.value || '');
}
if (node.type === 'inlineCode') {
return node.value || '';
}
if (node.children && Array.isArray(node.children)) {
return node.children.map(extractText).join('');
}
return node.value ? he.decode(node.value) : '';
}
// ═══════════════════════════════════════════════════════════════
// INLINE CONTENT RENDERER (for text inside blocks)
// ═══════════════════════════════════════════════════════════════
function renderInline(node, key = 0) {
if (!node) return null;
switch (node.type) {
case 'text':
return he.decode(node.value || '');
case 'strong':
return h(Text, { key, bold: true },
node.children?.map((c, i) => renderInline(c, i)));
case 'emphasis':
return h(Text, { key, italic: true },
node.children?.map((c, i) => renderInline(c, i)));
case 'inlineCode':
return h(Text, {
key,
color: theme.colors.warning,
backgroundColor: 'blackBright'
}, ` ${node.value} `);
case 'link':
return h(Text, { key, color: theme.colors.info, underline: true },
`${extractText(node)} (${node.url || ''})`);
case 'paragraph':
case 'heading':
// For nested content, just extract children
return node.children?.map((c, i) => renderInline(c, i));
default:
if (node.children) {
return node.children.map((c, i) => renderInline(c, i));
}
return node.value ? he.decode(node.value) : null;
}
}
// ═══════════════════════════════════════════════════════════════
// BLOCK RENDERERS - Each block gets its own Box with spacing
// ═══════════════════════════════════════════════════════════════
function renderHeading(node, index, width) {
const depth = node.depth || 1;
const colors = ['cyan', 'green', 'yellow', 'magenta', 'blue', 'white'];
const color = colors[Math.min(depth - 1, 5)];
const prefix = '#'.repeat(depth);
const text = extractText(node);
// CRITICAL: marginTop AND marginBottom ensure separation
return h(Box, {
key: `heading-${index}`,
marginTop: 1,
marginBottom: 1,
flexDirection: 'column',
width: width // Enforce width
},
h(Text, { bold: true, color, wrap: 'wrap' }, `${prefix} ${text}`)
);
}
function renderParagraph(node, index, width) {
// CRITICAL: marginBottom ensures paragraphs don't merge
return h(Box, {
key: `para-${index}`,
marginBottom: 1,
flexDirection: 'column',
width: width // Enforce width
},
h(Text, { wrap: 'wrap' },
node.children?.map((c, i) => renderInline(c, i)))
);
}
function renderCode(node, index, width) {
const lang = node.lang || 'text';
const code = he.decode(node.value || '');
// Supported languages
const SUPPORTED = ['javascript', 'typescript', 'python', 'java', 'html',
'css', 'json', 'yaml', 'bash', 'shell', 'sql', 'go', 'rust', 'plaintext'];
const safeLang = SUPPORTED.includes(lang.toLowerCase()) ? lang.toLowerCase() : 'plaintext';
try {
return h(Box, {
key: `code-${index}`,
marginTop: 1,
marginBottom: 1,
flexDirection: 'column',
width: width // Enforce width
},
h(Box, {
borderStyle: theme.borders.round,
borderColor: theme.colors.muted,
flexDirection: 'column',
paddingX: 1
},
h(Box, { marginBottom: 0 },
h(Text, { color: theme.colors.info, bold: true },
`${theme.icons.info} ${lang}`)
),
h(Highlight, { code, language: safeLang, theme: 'dracula' })
)
);
} catch (e) {
return h(Box, {
key: `code-${index}`,
marginTop: 1,
marginBottom: 1,
flexDirection: 'column'
},
h(Box, {
borderStyle: theme.borders.single,
borderColor: theme.colors.muted,
paddingX: 1
},
h(Text, {}, code)
)
);
}
}
function renderList(node, index, width) {
const ordered = node.ordered || false;
const items = node.children || [];
// Hanging indent: bullet in fixed-width column, text wraps aligned
return h(Box, {
key: `list-${index}`,
marginTop: 1,
marginBottom: 1,
flexDirection: 'column',
width: width // Enforce width
},
items.map((item, i) => {
const bullet = ordered ? `${i + 1}.` : '•';
const bulletWidth = ordered ? 4 : 3; // Fixed width for alignment
return h(Box, {
key: `item-${i}`,
flexDirection: 'row'
},
// Fixed-width bullet column for hanging indent
h(Box, { width: bulletWidth, flexShrink: 0 },
h(Text, { color: theme.colors.info }, bullet)
),
// Content wraps but stays aligned past bullet
h(Box, { flexDirection: 'column', flexGrow: 1, flexShrink: 1 },
item.children?.map((child, j) => {
if (child.type === 'paragraph') {
return h(Text, { key: j, wrap: 'wrap' },
child.children?.map((c, k) => renderInline(c, k)));
}
return renderBlock(child, j);
})
)
);
})
);
}
function renderBlockquote(node, index, width) {
// Decrease width for children by padding
const innerWidth = width ? width - 2 : undefined;
return h(Box, {
key: `quote-${index}`,
marginTop: 1,
marginBottom: 1,
flexDirection: 'row',
paddingLeft: 2,
width: width // Enforce width
},
h(Text, { color: theme.colors.muted }, '│ '),
h(Box, { flexDirection: 'column', dimColor: true, width: innerWidth },
node.children?.map((child, i) => renderBlock(child, i, innerWidth))
)
);
}
function renderThematicBreak(index) {
return h(Box, { key: `hr-${index}`, marginTop: 1, marginBottom: 1 },
h(Text, { color: theme.colors.muted }, '─'.repeat(40))
);
}
// ═══════════════════════════════════════════════════════════════
// MAIN BLOCK DISPATCHER
// ═══════════════════════════════════════════════════════════════
function renderBlock(node, index, width) {
if (!node) return null;
switch (node.type) {
case 'heading':
return renderHeading(node, index, width);
case 'paragraph':
return renderParagraph(node, index, width);
case 'code':
return renderCode(node, index, width);
case 'list':
return renderList(node, index, width);
case 'blockquote':
return renderBlockquote(node, index, width);
case 'thematicBreak':
return renderThematicBreak(index);
case 'html':
// Skip HTML nodes
return null;
default:
// For unknown types, try to extract text
const text = extractText(node);
if (text) {
return h(Box, { key: `unknown-${index}`, marginBottom: 1, width: width },
h(Text, { wrap: 'wrap' }, text)
);
}
return null;
}
}
// ═══════════════════════════════════════════════════════════════
// MAIN MARKDOWN COMPONENT
// ═══════════════════════════════════════════════════════════════
const Markdown = ({ children, syntaxTheme = 'dracula', width }) => {
if (!children || typeof children !== 'string') {
return null;
}
const content = children.trim();
if (!content) return null;
try {
// Parse markdown into AST
const processor = unified().use(remarkParse).use(remarkGfm);
const tree = processor.parse(he.decode(content));
// Get root children (top-level blocks)
const blocks = tree.children || [];
if (blocks.length === 0) {
return h(Box, { width },
h(Text, { wrap: 'wrap' }, content)
);
}
// Render each block with proper spacing
return h(Box, { flexDirection: 'column', width },
blocks.map((block, i) => renderBlock(block, i, width)).filter(Boolean)
);
} catch (err) {
// Fallback: render as plain text
return h(Text, { wrap: 'wrap' }, he.decode(content));
}
};
export default Markdown;