UNPKG

askeroo

Version:

A modern CLI prompt library with flow control, history navigation, and conditional prompts

280 lines 11.8 kB
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