askeroo
Version:
A modern CLI prompt library with flow control, history navigation, and conditional prompts
280 lines • 11.8 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Marked } from "marked";
import chalk from "chalk";
import { Text as InkText, Box } from "ink";
// Base function to create a MarkdownString
function createMarkdownString(content, theme) {
return {
__isMarkdown: true,
content,
theme,
};
}
// Tagged template literal function for md`content`
export function md(strings, ...values) {
// Combine template strings with interpolated values
let content = strings[0];
for (let i = 0; i < values.length; i++) {
content += String(values[i]) + strings[i + 1];
}
return createMarkdownString(content);
}
// Type guard to check if a value is a markdown string
export function isMarkdownString(value) {
return value && typeof value === "object" && value.__isMarkdown === true;
}
// Helper function to apply chalk styles to text
function applyChalkStyles(text, styles) {
const styleArray = styles.split(" ").filter((s) => s.length > 0);
let styledText = text;
// Apply each style using chalk
for (const style of styleArray) {
try {
// Handle background hex colors: bg#RRGGBB
if (style.startsWith("bg#")) {
const hexColor = style.slice(3); // Remove 'bg#' prefix
styledText = chalk.bgHex(hexColor)(styledText);
}
// Handle foreground hex colors: #RRGGBB
else if (style.startsWith("#")) {
styledText = chalk.hex(style)(styledText);
}
// Handle background colors and regular styles
else if (chalk[style] &&
typeof chalk[style] === "function") {
styledText = chalk[style](styledText);
}
}
catch (e) {
// Ignore invalid chalk styles
console.warn(`Invalid chalk style: ${style}`);
}
}
return styledText;
}
// Chalk token extension for marked
const chalkTokenExtension = {
name: "chalkToken",
level: "inline",
start(src) {
// Find the first occurrence of [text]{styles} pattern, handling nested brackets
const match = src.match(/\[(?:[^\[\]]*\[[^\]]*\])*[^\[\]]*\]\{([^}]+)\}/);
return match ? match.index : undefined;
},
tokenizer(src) {
// Regex to match [text]{style1 style2} pattern from the start, handling nested brackets
const rule = /^\[((?:[^\[\]]*\[[^\]]*\])*[^\[\]]*)\]\{([^}]+)\}/;
const match = rule.exec(src);
if (match) {
return {
type: "chalkToken",
raw: match[0],
text: match[1], // The text content
styles: match[2].trim(), // The chalk styles
};
}
},
renderer(token) {
// This will be handled in formatInlineTokens instead
return token.raw;
},
};
// Utility function to dedent content
function dedentContent(content) {
// Handle undefined or null content
if (!content)
return "";
// Split into lines
const lines = content.split("\n");
// Find the minimum indentation (excluding empty lines and the first line)
let minIndent = Infinity;
for (let i = 1; i < lines.length; i++) {
// Start from line 1, skip first line
const line = lines[i];
if (line.trim() === "")
continue; // Skip empty lines
const indent = line.match(/^(\s*)/)?.[1].length || 0;
minIndent = Math.min(minIndent, indent);
}
// If no indentation found, return as is
if (minIndent === Infinity) {
return content;
}
// Remove the common indentation from all lines
const dedentedLines = lines.map((line, index) => {
if (line.trim() === "")
return line; // Keep empty lines as is
if (index === 0)
return line; // Keep first line as is
return line.slice(minIndent);
});
return dedentedLines.join("\n");
}
// Format inline tokens from marked
function formatInlineTokens(tokens, theme) {
const elements = [];
let key = 0;
for (const token of tokens) {
switch (token.type) {
case "paragraph":
// Handle nested paragraph tokens (like in blockquotes and list items)
const paragraphElements = formatInlineTokens(token.tokens || [], theme);
elements.push(...paragraphElements);
break;
case "text":
if (token.tokens && token.tokens.length > 0) {
// Text token with nested tokens (like in list items)
const nestedElements = formatInlineTokens(token.tokens, theme);
elements.push(...nestedElements);
}
else {
// Simple text token
elements.push(_jsx(InkText, { color: theme.text, children: token.text }, key++));
}
break;
case "code":
case "codespan":
elements.push(_jsx(InkText, { color: theme.code, children: token.text }, key++));
break;
case "strong":
const strongText = token.tokens
? formatInlineTokens(token.tokens, theme)
: token.text;
elements.push(_jsx(InkText, { color: theme.text, bold: true, children: strongText }, key++));
break;
case "em":
const emphasisText = token.tokens
? formatInlineTokens(token.tokens, theme)
: token.text;
elements.push(_jsx(InkText, { color: theme.text, italic: true, children: emphasisText }, key++));
break;
case "link":
// Extract plain text from link token
const linkText = token.text || "";
const linkUrl = token.href || "";
// Show link text with URL in parentheses for CLI compatibility
// But only show URL in parentheses if the link text is different from the URL
const displayText = linkText === linkUrl
? linkText
: `${linkText} (${linkUrl})`;
elements.push(_jsx(InkText, { color: theme.link, underline: true, children: displayText }, key++));
break;
case "chalkToken":
// Apply chalk styles to the text, combining with theme color
const styledText = applyChalkStyles(token.text, `${theme.text} ${token.styles}`);
elements.push(_jsx(InkText, { children: styledText }, key++));
break;
default:
// For any unhandled inline token types, try to get text content
const text = token.text || "";
if (text) {
elements.push(_jsx(InkText, { color: theme.text, children: text }, key++));
}
break;
}
}
return elements;
}
// Create a custom marked instance with chalk token extension
const customMarked = new Marked({
extensions: [chalkTokenExtension],
});
// Parse markdown content into React elements
export function parseMarkdown(content, theme = {}) {
const defaultTheme = {
heading: "cyan",
text: "white",
code: "cyan",
link: "blue",
strong: "bold",
emphasis: "italic",
...theme,
};
// Automatically dedent the content
const dedentedContent = dedentContent(content || "");
// Parse markdown into tokens using custom marked instance
const tokens = customMarked.lexer(dedentedContent);
const elements = [];
let key = 0;
// Process tokens
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const nextToken = tokens[i + 1];
switch (token.type) {
case "heading":
const level = token.depth || 1;
const headingText = token.text;
if (level === 1) {
elements.push(_jsx(InkText, { color: defaultTheme.heading, bold: true, children: headingText }, key++));
}
else if (level === 2) {
elements.push(_jsx(InkText, { color: "blue", bold: true, children: headingText }, key++));
}
else if (level === 3) {
elements.push(_jsx(InkText, { bold: true, children: headingText }, key++));
}
else {
// H4: Bullet point style with dots
elements.push(_jsx(InkText, { dimColor: true, bold: true, children: headingText }, key++));
}
// Add spacing after heading if next token is not a space and not the last token
if (nextToken &&
nextToken.type !== "space" &&
nextToken.type !== "heading") {
elements.push(_jsx(Box, { height: 1 }, key++));
}
break;
case "code":
const codeText = token.text;
if (token.lang) {
// Fenced code block
elements.push(_jsx(Box, { marginLeft: 2, borderStyle: "round", borderColor: "gray", children: _jsx(InkText, { color: defaultTheme.code, children: codeText }) }, key++));
}
else {
// Inline code
elements.push(_jsx(InkText, { color: defaultTheme.code, backgroundColor: "gray", children: codeText }, key++));
}
break;
case "list":
const listItems = token.items || [];
listItems.forEach((item, index) => {
let marker;
if (token.ordered) {
// Use the actual item number for ordered lists
marker = `${index + 1}.`;
}
else {
// Use bullet point for unordered lists
marker = "•";
}
// Process the tokens within the list item
const itemTokens = item.tokens || [];
const formattedItem = formatInlineTokens(itemTokens, defaultTheme);
elements.push(_jsxs(Box, { marginLeft: 2, children: [_jsxs(InkText, { color: defaultTheme.text, children: [marker, " "] }), formattedItem] }, key++));
});
break;
case "blockquote":
// Process the paragraph tokens within the blockquote
const paragraphTokens = token.tokens || [];
const formattedQuote = formatInlineTokens(paragraphTokens, defaultTheme);
elements.push(_jsxs(Box, { children: [_jsx(InkText, { color: defaultTheme.text, children: "> " }), formattedQuote] }, key++));
break;
case "paragraph":
const formattedText = formatInlineTokens(token.tokens || [], defaultTheme);
elements.push(_jsx(Box, { children: formattedText }, key++));
break;
case "space":
elements.push(_jsx(Box, { height: 1 }, key++));
break;
default:
// For any unhandled token types, try to get text content
const text = token.text || "";
if (text.trim()) {
elements.push(_jsx(InkText, { color: defaultTheme.text, children: text }, key++));
}
break;
}
}
return elements;
}
//# sourceMappingURL=markdown.js.map