#include "html.hpp" namespace html { int precision_level = 6; // thanks claude for this interface. void gen_html(std::ostream& out, const std::shared_ptr<cmd::Node>& node) { out << "<!DOCTYPE html>\n"; out << "<html>\n"; out << "<head>\n"; out << " <meta charset='utf-8'>\n"; out << " <meta name='viewport' content='width=device-width, initial-scale=1.0'>\n"; out << " <title>Shell AST Visualization</title>\n"; out << " <link rel='preconnect' href='https://fonts.googleapis.com'>\n"; out << " <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>\n"; out << " <link href='https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap' rel='stylesheet'>\n"; out << " <style>\n"; out << " :root {\n"; out << " --bg-primary: #0f172a;\n"; out << " --bg-secondary: #1e293b;\n"; out << " --bg-tertiary: #334155;\n"; out << " --text-primary: #f8fafc;\n"; out << " --text-secondary: #cbd5e1;\n"; out << " --accent-primary: #38bdf8;\n"; out << " --accent-secondary: #0ea5e9;\n"; out << " --accent-tertiary: #0284c7;\n"; out << " --highlight: #2563eb;\n"; out << " --success: #10b981;\n"; out << " --warning: #f59e0b;\n"; out << " --error: #ef4444;\n"; out << " --border-radius: 8px;\n"; out << " --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n"; out << " --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n"; out << " --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n"; out << " }\n"; out << " * {\n"; out << " margin: 0;\n"; out << " padding: 0;\n"; out << " box-sizing: border-box;\n"; out << " }\n"; out << " body {\n"; out << " font-family: 'Inter', system-ui, -apple-system, sans-serif;\n"; out << " background-color: var(--bg-primary);\n"; out << " color: var(--text-primary);\n"; out << " line-height: 1.6;\n"; out << " padding: 2rem;\n"; out << " transition: all 0.3s ease;\n"; out << " }\n"; out << " header {\n"; out << " margin-bottom: 2rem;\n"; out << " border-bottom: 1px solid var(--bg-tertiary);\n"; out << " padding-bottom: 1rem;\n"; out << " }\n"; out << " h1 {\n"; out << " font-size: 2.25rem;\n"; out << " font-weight: 700;\n"; out << " color: var(--accent-primary);\n"; out << " margin-bottom: 0.5rem;\n"; out << " letter-spacing: -0.025em;\n"; out << " }\n"; out << " h2, h3, h4, h5 {\n"; out << " color: var(--accent-primary);\n"; out << " font-weight: 600;\n"; out << " margin: 1rem 0 0.5rem 0;\n"; out << " letter-spacing: -0.01em;\n"; out << " }\n"; out << " h3 {\n"; out << " font-size: 1.25rem;\n"; out << " font-weight: 500;\n"; out << " }\n"; out << " .node {\n"; out << " margin: 1rem 0;\n"; out << " padding: 1.25rem;\n"; out << " border-radius: var(--border-radius);\n"; out << " background-color: var(--bg-secondary);\n"; out << " border: 1px solid var(--bg-tertiary);\n"; out << " box-shadow: var(--shadow);\n"; out << " transition: all 0.2s ease-in-out;\n"; out << " }\n"; out << " .node:hover {\n"; out << " transform: translateY(-2px);\n"; out << " box-shadow: var(--shadow-lg);\n"; out << " }\n"; out << " table {\n"; out << " border-collapse: separate;\n"; out << " border-spacing: 0;\n"; out << " width: auto; /* Allow table to expand based on content */\n"; out << " min-width: 100%; /* Ensure it takes at least full width */\n"; out << " margin: 1rem 0;\n"; out << " border-radius: var(--border-radius);\n"; out << " overflow: visible; /* Allow content to overflow visually */\n"; out << " box-shadow: var(--shadow-sm);\n"; out << " table-layout: auto; /* Let browser determine column widths */\n"; out << " }\n"; out << " th, td {\n"; out << " white-space: nowrap; /* Prevent text wrapping */\n"; out << " vertical-align: top;\n"; out << " overflow: visible; /* Allow content to overflow cell bounds */\n"; out << " }\n"; out << " th {\n"; out << " background-color: var(--bg-tertiary);\n"; out << " color: var(--text-primary);\n"; out << " text-align: left;\n"; out << " padding: 0.75rem 1rem;\n"; out << " font-weight: 600;\n"; out << " text-transform: uppercase;\n"; out << " font-size: 0.75rem;\n"; out << " letter-spacing: 0.05em;\n"; out << " }\n"; out << " td {\n"; out << " padding: 0.75rem 1rem;\n"; out << " border-top: 1px solid var(--bg-tertiary);\n"; out << " font-family: 'Fira Code', monospace;\n"; out << " }\n"; out << " /* Add a container for horizontal scrolling */\n"; out << " .table-container {\n"; out << " width: 100%;\n"; out << " overflow-x: auto; /* Enable horizontal scrollbar if needed */\n"; out << " margin: 1rem 0;\n"; out << " }\n"; out << " /* Node type styles */\n"; out << " .command { background-color: rgba(56, 189, 248, 0.1); border-left: 3px solid var(--accent-primary); }\n"; out << " .pipeline { background-color: rgba(16, 185, 129, 0.1); border-left: 3px solid var(--success); }\n"; out << " .redirection { background-color: rgba(245, 158, 11, 0.1); border-left: 3px solid var(--warning); }\n"; out << " .sequence { background-color: rgba(99, 102, 241, 0.1); border-left: 3px solid #6366f1; }\n"; out << " .logical-and { background-color: rgba(139, 92, 246, 0.1); border-left: 3px solid #8b5cf6; }\n"; out << " .logical-or { background-color: rgba(251, 113, 133, 0.1); border-left: 3px solid #fb7185; }"; out << " .if-statement { background-color: rgba(236, 72, 153, 0.1); border-left: 3px solid #ec4899; }\n"; out << " .while-statement { background-color: rgba(248, 113, 113, 0.1); border-left: 3px solid #f87171; }\n"; out << " .for-statement { background-color: rgba(251, 146, 60, 0.1); border-left: 3px solid #fb923c; }\n"; out << " .variable-assignment { background-color: rgba(20, 184, 166, 0.1); border-left: 3px solid #14b8a6; }\n"; out << " .string-literal { background-color: rgba(16, 185, 129, 0.1); border-left: 3px solid #10b981; }\n"; out << " .number-literal { background-color: rgba(56, 189, 248, 0.1); border-left: 3px solid #38bdf8; }\n"; out << " .variable-reference { background-color: rgba(139, 92, 246, 0.1); border-left: 3px solid #8b5cf6; }\n"; out << " .binary-expression { background-color: rgba(236, 72, 153, 0.1); border-left: 3px solid #ec4899; }\n"; out << " .unary-expression { background-color: rgba(248, 113, 113, 0.1); border-left: 3px solid #f87171; }\n"; out << " .command-substitution { background-color: rgba(251, 146, 60, 0.1); border-left: 3px solid #fb923c; }\n"; out << " .return { background-color: rgba(234, 88, 12, 0.1); border-left: 3px solid #ea580c; }\n"; out << " /* Text styles */\n"; out << " .filename { color: #fbbf24; font-style: italic; }\n"; out << " .operator { color: #e11d48; font-weight: bold; }\n"; out << " .literal { color: #10b981; }\n"; out << " .variable { color: #38bdf8; }\n"; out << " .symbol { color: #ec4899; font-weight: 500; }\n"; out << " .command-substitution { color: #fb923c; }\n"; out << " /* Responsive design */\n"; out << " @media (max-width: 768px) {\n"; out << " body { padding: 1rem; }\n"; out << " h1 { font-size: 1.75rem; }\n"; out << " .node { padding: 1rem; }\n"; out << " }\n"; out << " /* Collapsible sections */\n"; out << " .node .value, .node .body, .node .right, .node .left {\n"; out << " position: relative;\n"; out << " padding-left: 1.25rem;\n"; out << " display: block;\n"; out << " }\n"; out << " .node h3 {\n"; out << " cursor: pointer;\n"; out << " user-select: none;\n"; out << " display: flex;\n"; out << " align-items: center;\n"; out << " }\n"; out << " .node h3::before {\n"; out << " content: '▼';\n"; out << " display: inline-block;\n"; out << " margin-right: 0.5rem;\n"; out << " font-size: 0.75rem;\n"; out << " transition: transform 0.2s ease;\n"; out << " transform: rotate(var(--rotate, 0deg));\n"; out << " }\n"; out << " .node h3.collapsed::before {\n"; out << " transform: rotate(-90deg);\n"; out << " }\n"; out << " </style>\n"; out << " <script>\n"; out << " document.addEventListener('DOMContentLoaded', function() {\n"; out << " // Wrap tables in scrollable containers\n"; out << " document.querySelectorAll('main > table, .node > table').forEach(table => {\n"; out << " // Check if it's already wrapped\n"; out << " if (!table.parentElement || !table.parentElement.classList.contains('table-container')) {\n"; out << " const container = document.createElement('div');\n"; out << " container.className = 'table-container';\n"; out << " // Insert the container before the table\n"; out << " table.parentNode.insertBefore(container, table);\n"; out << " // Move the table inside the container\n"; out << " container.appendChild(table);\n"; out << " }\n"; out << " // Force reflow might still be useful\n"; out << " void table.offsetHeight;\n"; out << " });\n\n"; out << " // Existing collapsible logic\n"; out << " document.querySelectorAll('.node h3').forEach(header => {\n"; out << " header.addEventListener('click', function() {\n"; out << " const content = this.nextElementSibling;\n"; out << " if (content) {\n"; out << " const isVisible = getComputedStyle(content).display !== 'none';\n"; out << " content.style.display = isVisible ? 'none' : 'block';\n"; out << " this.classList.toggle('collapsed', isVisible);\n"; out << " }\n"; out << " });\n"; out << " });\n"; out << " });\n"; out << " </script>\n"; out << "</head>\n"; out << "<body>\n"; out << " <header>\n"; out << " <h1>Shell AST Visualization</h1>\n"; out << " <p>Interactive abstract syntax tree representation of your shell script</p>\n"; out << " </header>\n"; out << " <main>\n"; node->print(out, 4); out << " </main>\n"; out << "</body>\n"; out << "</html>\n"; } void gen_html_color(std::ostream &out, std::shared_ptr<cmd::Node> &node) { out << "<!DOCTYPE html>\n"; out << "<html>\n"; out << "<head>\n"; out << " <meta charset='utf-8'>\n"; out << " <meta name='viewport' content='width=device-width, initial-scale=1.0'>\n"; out << " <title>MXCMD Code Syntax Highlighting</title>\n"; out << " <link rel='preconnect' href='https://fonts.googleapis.com'>\n"; out << " <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>\n"; out << " <link href='https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap' rel='stylesheet'>\n"; out << " <style>\n"; out << " :root {\n"; out << " --bg-primary: #0f172a;\n"; out << " --bg-secondary: #1e293b;\n"; out << " --text-primary: #f8fafc;\n"; out << " --border-color: #334155;\n"; out << " --keyword: #ec4899;\n"; // Pink for keywords out << " --function: #38bdf8;\n"; // Blue for functions out << " --variable: #67e8f9;\n"; // Cyan for variables out << " --number: #10b981;\n"; // Green for numbers out << " --string: #fbbf24;\n"; // Yellow for strings out << " --comment: #64748b;\n"; // Slate for comments out << " --operator: #e11d48;\n"; // Red for operators out << " --punctuation: #cbd5e1;\n"; // Light gray for punctuation out << " --identifier: #f8fafc;\n"; // White for identifiers out << " --bracket: #94a3b8;\n"; // Gray for brackets out << " --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n"; out << " }\n"; out << " * {\n"; out << " margin: 0;\n"; out << " padding: 0;\n"; out << " box-sizing: border-box;\n"; out << " }\n"; out << " body {\n"; out << " font-family: 'Inter', system-ui, -apple-system, sans-serif;\n"; out << " background-color: var(--bg-primary);\n"; out << " color: var(--text-primary);\n"; out << " line-height: 1.6;\n"; out << " padding: 2rem;\n"; out << " transition: all 0.3s ease;\n"; out << " }\n"; out << " header {\n"; out << " margin-bottom: 2rem;\n"; out << " border-bottom: 1px solid var(--border-color);\n"; out << " padding-bottom: 1rem;\n"; out << " }\n"; out << " h1 {\n"; out << " font-size: 2.25rem;\n"; out << " font-weight: 700;\n"; out << " color: var(--function);\n"; out << " margin-bottom: 0.5rem;\n"; out << " }\n"; out << " .code-container {\n"; out << " background-color: var(--bg-secondary);\n"; out << " border-radius: 8px;\n"; out << " padding: 1.5rem;\n"; out << " box-shadow: var(--shadow);\n"; out << " margin-bottom: 2rem;\n"; out << " overflow-x: auto;\n"; out << " }\n"; out << " pre.code {\n"; out << " font-family: 'Fira Code', monospace;\n"; out << " font-size: 14px;\n"; out << " line-height: 1.2;\n"; out << " white-space: pre;\n"; out << " font-variant-ligatures: none; \n"; out << " }\n"; out << " /* Syntax highlighting classes */\n"; out << " .keyword { color: var(--keyword); }\n"; out << " .function { color: var(--function); }\n"; out << " .variable { color: var(--variable); }\n"; out << " .string { color: var(--string); }\n"; out << " .number { color: var(--number); }\n"; out << " .comment { color: var(--comment); font-style: italic; }\n"; out << " .operator { color: var(--operator); }\n"; out << " .punctuation { color: var(--punctuation); }\n"; out << " .identifier { color: var(--identifier); }\n"; out << " .bracket { color: var(--bracket); }\n"; out << " .line-number {\n"; out << " display: inline-block;\n"; out << " width: 2rem;\n"; out << " padding-right: 1rem;\n"; out << " text-align: right;\n"; out << " color: var(--comment);\n"; out << " user-select: none;\n"; out << " }\n"; out << " .code-line {\n"; out << " display: inline-block;\n"; // Change from block to inline-block out << " }\n"; out << " </style>\n"; out << "</head>\n"; out << "<body>\n"; out << " <header>\n"; out << " <h1>MXCMD Syntax Highlighting</h1>\n"; out << " <p>Colorized code representation</p>\n"; out << " </header>\n"; out << " <div class=\"code-container\">\n"; out << " <pre class=\"code\">\n"; class CodeFormatter { public: static std::string escapeHtml(const std::string& input) { std::string escaped; escaped.reserve(input.size()); for (char c : input) { switch (c) { case '<': escaped += "<"; break; case '>': escaped += ">"; break; case '&': escaped += "&"; break; case '"': escaped += """; break; case '\'': escaped += "'"; break; default: escaped += c; } } return escaped; } static std::string formatEscapeSequences(const std::string& input) { std::string result; result.reserve(input.size() * 2); for (size_t i = 0; i < input.size(); ++i) { if (i + 1 < input.size() && input[i] == '\\') { switch (input[i+1]) { case '"': result += "\\\""; i++; break; case '\'': result += "\\'"; i++; break; case 'n': result += "\\n"; i++; break; case 't': result += "\\t"; i++; break; case 'r': result += "\\r"; i++; break; case 'f': result += "\\f"; i++; break; case 'v': result += "\\v"; i++; break; case 'b': result += "\\b"; i++; break; case '\\': result += "\\\\"; i++; break; default: result += "\\"; break; } } else { switch (input[i]) { case '\n': result += "\\n"; break; case '\t': result += "\\t"; break; case '\r': result += "\\r"; break; case '\f': result += "\\f"; break; case '\v': result += "\\v"; break; case '\b': result += "\\b"; break; case '\\': result += "\\\\"; break; case '<': result += "<"; break; case '>': result += ">"; break; case '&': result += "&"; break; case '"': result += "\""; break; case '\'': result += "'"; break; default: result += input[i]; break; } } } return result; } static std::string nodeToString(const std::shared_ptr<cmd::Node>& node) { if (!node) return ""; return node->toString(); } static std::string formatNumberWithPrecision(const std::string& value) { std::ostringstream numStr; try { double num = std::stod(value); if (num == std::floor(num)) { numStr << std::fixed << std::setprecision(0); } else { numStr << std::fixed << std::setprecision(precision_level); } } catch(const std::exception&) {} try { double num = std::stod(value); numStr << num; return "<span class=\"number\">" + escapeHtml(numStr.str()) + "</span>"; } catch(const std::exception&) { return "<span class=\"identifier\">" + escapeHtml(value) + "</span>"; } } static void format(std::ostream& out, const std::shared_ptr<cmd::Node>& node) { std::vector<std::string> formattedLines; formatBlock(formattedLines, node, 0); for (size_t i = 0; i < formattedLines.size(); ++i) { out << "<span class=\"line-number\">" << (i + 1) << "</span>"; out << formattedLines[i] << "\n"; } } static std::string formatNodeInline(const std::shared_ptr<cmd::Node>& node) { if (!node) { return "<span class=\"comment\">[null]</span>"; } if (auto expr = std::dynamic_pointer_cast<cmd::Expression>(node)) { return formatExpressionInline(expr); } if (auto cmd = std::dynamic_pointer_cast<cmd::Command>(node)) { std::string result = "<span class=\"identifier\">" + escapeHtml(cmd->name) + "</span>"; for (size_t arg_index = 0; arg_index < cmd->args.size(); arg_index++) { const auto& arg = cmd->args[arg_index]; result += " "; if (arg.type == cmd::ARG_LITERAL) { std::string val = arg.value; if (val.size() > 1 && val[0] == '-' && val.find_first_not_of("0123456789.", 1) == std::string::npos && val.find_first_of("0123456789", 1) != std::string::npos) { result += formatNumberWithPrecision(val); continue; } } if (arg.type == cmd::ARG_VARIABLE) { result += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.type == cmd::ARG_STRING_LITERAL) { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else if (arg.type == cmd::ARG_COMMAND_SUBST) { result += "<span class=\"operator\">$(</span>"; if (arg.cmdNode) { result += formatNodeInline(arg.cmdNode); } else { result += "<span class=\"command-substitution\">" + escapeHtml(arg.value.substr(2, arg.value.length() - 3)) + "</span>"; } result += "<span class=\"operator\">)</span>"; } else if (cmd->name == "printf" || cmd->name == "echo" || cmd->name == "puts" || cmd->name == "print" || cmd->name == "say") { if (arg_index == 0) { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else { if (arg.type == cmd::ARG_VARIABLE) { result += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.value.find_first_of("0123456789") == 0 && arg.value.find_first_not_of("0123456789.") == std::string::npos) { result += formatNumberWithPrecision(arg.value); } else { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } } } else { if (arg.type == cmd::ARG_VARIABLE) { result += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.value.find(' ') != std::string::npos || arg.value.find('\t') != std::string::npos || arg.value.find('\n') != std::string::npos || arg.value.find('\\') != std::string::npos || arg.value.find('%') != std::string::npos || arg.value.find('"') != std::string::npos || arg.value.find('\'') != std::string::npos) { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else if (arg.value.find_first_of("0123456789") == 0 && arg.value.find_first_not_of("0123456789.") == std::string::npos) { result += formatNumberWithPrecision(arg.value); } else { result += "<span class=\"identifier\">" + escapeHtml(arg.value) + "</span>"; } } } return result; } if (auto seq = std::dynamic_pointer_cast<cmd::Sequence>(node)) { std::string result; for (size_t i = 0; i < seq->commands.size(); i++) { if (i > 0) result += " <span class=\"operator\">;</span> "; result += formatNodeInline(seq->commands[i]); } return result; } else if (auto pipeline = std::dynamic_pointer_cast<cmd::Pipeline>(node)) { std::string result; for (size_t i = 0; i < pipeline->commands.size(); i++) { if (i > 0) result += " <span class=\"operator\">|</span> "; result += formatNodeInline(pipeline->commands[i]); } return result; } else if (auto logAnd = std::dynamic_pointer_cast<cmd::LogicalAnd>(node)) { return formatNodeInline(logAnd->left) + " <span class=\"operator\">&&</span> " + formatNodeInline(logAnd->right); } else if (auto logOr = std::dynamic_pointer_cast<cmd::LogicalOr>(node)) { return formatNodeInline(logOr->left) + " <span class=\"operator\">||</span> " + formatNodeInline(logOr->right); } return nodeToString(node); } private: static void formatBlock(std::vector<std::string>& lines, const std::shared_ptr<cmd::Node>& node, int indentLevel) { std::string indent(indentLevel * 2, ' '); if (!node) { lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"comment\">// null node</span></span>"); return; } if (auto seq = std::dynamic_pointer_cast<cmd::Sequence>(node)) { for (const auto& cmd : seq->commands) { formatBlock(lines, cmd, indentLevel); } } else if (auto cmd = std::dynamic_pointer_cast<cmd::Command>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"identifier\">" + escapeHtml(cmd->name) + "</span>"; for (size_t arg_index = 0; arg_index < cmd->args.size(); arg_index++) { const auto& arg = cmd->args[arg_index]; line += " "; if (arg.type == cmd::ARG_LITERAL) { std::string val = arg.value; if (val.size() > 1 && val[0] == '-' && val.find_first_not_of("0123456789.", 1) == std::string::npos && val.find_first_of("0123456789", 1) != std::string::npos) { line += formatNumberWithPrecision(val); continue; } } if (arg.type == cmd::ARG_VARIABLE) { line += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.type == cmd::ARG_STRING_LITERAL) { line += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else if (arg.type == cmd::ARG_COMMAND_SUBST) { line += "<span class=\"operator\">$(</span>"; if (arg.cmdNode) { line += formatNodeInline(arg.cmdNode); } else { line += "<span class=\"command-substitution\">" + escapeHtml(arg.value.substr(2, arg.value.length() - 3)) + "</span>"; } line += "<span class=\"operator\">)</span>"; } else if (arg.value.find_first_of("0123456789") == 0 && arg.value.find_first_not_of("0123456789.") == std::string::npos) { line += formatNumberWithPrecision(arg.value); } else if (arg.value.find(' ') != std::string::npos || arg.value.find('\t') != std::string::npos || arg.value.find('\n') != std::string::npos || arg.value.find('\\') != std::string::npos || arg.value.find('%') != std::string::npos || arg.value.find('"') != std::string::npos || arg.value.find('\'') != std::string::npos) { line += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else { line += "<span class=\"identifier\">" + escapeHtml(arg.value) + "</span>"; } } line += "</span>"; lines.push_back(line); } else if (auto varAssign = std::dynamic_pointer_cast<cmd::VariableAssignment>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"variable\">" + escapeHtml(varAssign->name) + "</span> "; line += "<span class=\"operator\">=</span> "; if (auto commandSubst = std::dynamic_pointer_cast<cmd::CommandSubstitution>(varAssign->value)) { line += "<span class=\"operator\">$(</span>"; if (commandSubst->command) { line += formatNodeInline(commandSubst->command); } else { line += "<span class=\"comment\">/* empty command */</span>"; } line += "<span class=\"operator\">)</span>"; } else if (auto exprNode = std::dynamic_pointer_cast<cmd::Expression>(varAssign->value)) { line += formatExpressionInline(exprNode); } else { line += "<span class=\"comment\">/* non-expression value */</span>"; } line += "</span>"; lines.push_back(line); } else if (auto ifStmt = std::dynamic_pointer_cast<cmd::IfStatement>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"keyword\">if</span> "; if (!ifStmt->branches.empty()) { line += formatNodeInline(ifStmt->branches[0].condition); } line += "</span>"; lines.push_back(line); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">then</span></span>"); if (!ifStmt->branches.empty()) { formatBlock(lines, ifStmt->branches[0].action, indentLevel + 2); } for (size_t i = 1; i < ifStmt->branches.size(); i++) { std::string elifLine = "<span class=\"code-line\">" + indent; elifLine += "<span class=\"keyword\">elif</span> "; elifLine += formatNodeInline(ifStmt->branches[i].condition); elifLine += "</span>"; lines.push_back(elifLine); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">then</span></span>"); formatBlock(lines, ifStmt->branches[i].action, indentLevel + 2); } if (ifStmt->elseAction) { lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">else</span></span>"); formatBlock(lines, ifStmt->elseAction, indentLevel + 2); } lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">fi</span></span>"); } else if (auto forStmt = std::dynamic_pointer_cast<cmd::ForStatement>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"keyword\">for</span> "; line += "<span class=\"variable\">" + escapeHtml(forStmt->variable) + "</span> "; line += "<span class=\"keyword\">in</span> "; for (const auto& val : forStmt->values) { if (val.type == cmd::ARG_VARIABLE) { line += "<span class=\"variable\">" + escapeHtml(val.value) + "</span> "; } else if (val.type == cmd::ARG_COMMAND_SUBST) { line += "<span class=\"operator\">$(</span>"; if (val.cmdNode) { line += formatNodeInline(val.cmdNode); } else { line += "<span class=\"command-substitution\">" + escapeHtml(val.value.substr(2, val.value.length() - 3)) + "</span>"; } line += "<span class=\"operator\">)</span> "; } else { line += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(val.value)) + ""</span> "; } } line += "</span>"; lines.push_back(line); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">do</span></span>"); formatBlock(lines, forStmt->body, indentLevel + 2); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">done</span></span>"); } else if (auto redirNode = std::dynamic_pointer_cast<cmd::Redirection>(node)) { std::string line = "<span class=\"code-line\">" + indent; if (auto cmd = std::dynamic_pointer_cast<cmd::Command>(redirNode->command)) { line += "<span class=\"identifier\">" + escapeHtml(cmd->name) + "</span>"; for (size_t arg_index = 0; arg_index < cmd->args.size(); arg_index++) { const auto& arg = cmd->args[arg_index]; line += " "; if (arg.type == cmd::ARG_VARIABLE) { line += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.type == cmd::ARG_STRING_LITERAL) { line += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else if (arg.value.find_first_of("0123456789") == 0 && arg.value.find_first_not_of("0123456789.") == std::string::npos) { line += formatNumberWithPrecision(arg.value); } else if (arg.value.find(' ') != std::string::npos || arg.value.find('\t') != std::string::npos || arg.value.find('\n') != std::string::npos || arg.value.find('\\') != std::string::npos || arg.value.find('%') != std::string::npos || arg.value.find('"') != std::string::npos || arg.value.find('\'') != std::string::npos) { line += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else { line += "<span class=\"identifier\">" + escapeHtml(arg.value) + "</span>"; } } } else { line += nodeToString(redirNode->command); } std::string op; switch (redirNode->type) { case cmd::Redirection::INPUT: op = "<"; break; case cmd::Redirection::OUTPUT: op = ">"; break; case cmd::Redirection::APPEND: op = ">>"; break; default: op = "?"; break; } line += " <span class=\"operator\">" + escapeHtml(op) + "</span> "; line += "<span class=\"string\">" + escapeHtml(formatEscapeSequences(redirNode->file)) + "</span>"; line += "</span>"; lines.push_back(line); } else if (auto pipeNode = std::dynamic_pointer_cast<cmd::Pipeline>(node)) { std::string line = "<span class=\"code-line\">" + indent; if (!pipeNode->commands.empty() && pipeNode->commands.size() >= 2) { if (auto cmd = pipeNode->commands[0]) { line += "<span class=\"identifier\">" + escapeHtml(cmd->name) + "</span>"; } line += " <span class=\"operator\">|</span> "; if (auto cmd = pipeNode->commands[1]) { line += "<span class=\"identifier\">" + escapeHtml(cmd->name) + "</span>"; } } line += "</span>"; lines.push_back(line); } else if (auto funcDef = std::dynamic_pointer_cast<cmd::CommandDefinition>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"keyword\">define</span> "; line += "<span class=\"function\">" + escapeHtml(funcDef->name) + "</span>"; line += "<span class=\"bracket\">(</span>"; for (size_t i = 0; i < funcDef->parameters.size(); ++i) { if (i > 0) { line += "<span class=\"punctuation\">, </span>"; } line += "<span class=\"variable\">" + escapeHtml(funcDef->parameters[i]) + "</span>"; } line += "<span class=\"bracket\">)</span>"; line += "</span>"; lines.push_back(line); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">begin</span></span>"); formatBlock(lines, funcDef->body, indentLevel + 2); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">end</span></span>"); } else if (auto whileStmt = std::dynamic_pointer_cast<cmd::WhileStatement>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"keyword\">while</span> "; if (auto condExpr = std::dynamic_pointer_cast<cmd::Expression>(whileStmt->condition)) { line += formatExpressionInline(condExpr); } else { line += nodeToString(whileStmt->condition); } line += "</span>"; lines.push_back(line); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">do</span></span>"); formatBlock(lines, whileStmt->body, indentLevel + 2); lines.push_back("<span class=\"code-line\">" + indent + "<span class=\"keyword\">done</span></span>"); } else if (auto logicalAnd = std::dynamic_pointer_cast<cmd::LogicalAnd>(node)) { formatBlock(lines, logicalAnd->left, indentLevel); if (!lines.empty()) { std::string lastLine = lines.back(); lines.pop_back(); lastLine += " <span class=\"operator\">&&</span> "; lastLine += formatNodeInline(logicalAnd->right); lines.push_back(lastLine); } } else if (auto logicalOr = std::dynamic_pointer_cast<cmd::LogicalOr>(node)) { formatBlock(lines, logicalOr->left, indentLevel); if (!lines.empty()) { std::string lastLine = lines.back(); lines.pop_back(); lastLine += " <span class=\"operator\">||</span> "; lastLine += formatNodeInline(logicalOr->right); lines.push_back(lastLine); } } else if (auto unaryExpr = std::dynamic_pointer_cast<cmd::UnaryExpression>(node)) { std::string line = "<span class=\"code-line\">" + indent; if (unaryExpr->op == cmd::UnaryExpression::INCREMENT || unaryExpr->op == cmd::UnaryExpression::DECREMENT) { std::string opStr = (unaryExpr->op == cmd::UnaryExpression::INCREMENT) ? "++" : "--"; if (unaryExpr->position == cmd::UnaryExpression::PREFIX) { line += "<span class=\"operator\">" + opStr + "</span>"; if (auto varRef = std::dynamic_pointer_cast<cmd::VariableReference>(unaryExpr->operand)) { line += "<span class=\"variable\">" + escapeHtml(varRef->name) + "</span>"; } } else { if (auto varRef = std::dynamic_pointer_cast<cmd::VariableReference>(unaryExpr->operand)) { line += "<span class=\"variable\">" + escapeHtml(varRef->name) + "</span>"; } line += "<span class=\"operator\">" + opStr + "</span>"; } } else if (unaryExpr->op == cmd::UnaryExpression::NEGATE) { if (auto numLit = std::dynamic_pointer_cast<cmd::NumberLiteral>(unaryExpr->operand)) { line += formatNumberWithPrecision(std::to_string(-numLit->value)); } else { line += "<span class=\"operator\">-</span>"; line += formatExpressionInline(std::dynamic_pointer_cast<cmd::Expression>(unaryExpr->operand)); } } line += "</span>"; lines.push_back(line); } else if (auto returnStmt = std::dynamic_pointer_cast<cmd::Return>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"keyword\">return</span> "; if (auto expr = std::dynamic_pointer_cast<cmd::Expression>(returnStmt->value)) { line += formatExpressionInline(expr); } line += "</span>"; lines.push_back(line); } else if (auto breakStmt = std::dynamic_pointer_cast<cmd::Break>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"keyword\">break</span>"; line += "</span>"; lines.push_back(line); } else if (auto continueStmt = std::dynamic_pointer_cast<cmd::Continue>(node)) { std::string line = "<span class=\"code-line\">" + indent; line += "<span class=\"keyword\">continue</span>"; line += "</span>"; lines.push_back(line); } else { lines.push_back("<span class=\"code-line\">" + indent + nodeToString(node) + "</span>"); } } static std::string formatExpressionInline(const std::shared_ptr<cmd::Expression>& expr) { if (!expr) { return "<span class=\"comment\">[null]</span>"; } if (auto strLit = std::dynamic_pointer_cast<cmd::StringLiteral>(expr)) { return "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(strLit->value)) + ""</span>"; } else if (auto numLit = std::dynamic_pointer_cast<cmd::NumberLiteral>(expr)) { return formatNumberWithPrecision(std::to_string(numLit->value)); } else if (auto varRef = std::dynamic_pointer_cast<cmd::VariableReference>(expr)) { return "<span class=\"variable\">" + escapeHtml(varRef->name) + "</span>"; } else if (auto binExpr = std::dynamic_pointer_cast<cmd::BinaryExpression>(expr)) { std::string result; if (auto leftExpr = std::dynamic_pointer_cast<cmd::Expression>(binExpr->left)) { result += formatExpressionInline(leftExpr); } else { result += nodeToString(binExpr->left); } std::string opStr; switch (binExpr->op) { case cmd::BinaryExpression::ADD: opStr = "+"; break; case cmd::BinaryExpression::SUBTRACT: opStr = "-"; break; case cmd::BinaryExpression::MULTIPLY: opStr = "*"; break; case cmd::BinaryExpression::DIVIDE: opStr = "/"; break; case cmd::BinaryExpression::MODULO: opStr = "%"; break; default: opStr = "?"; break; } result += " <span class=\"operator\">" + escapeHtml(opStr) + "</span> "; if (auto rightExpr = std::dynamic_pointer_cast<cmd::Expression>(binExpr->right)) { result += formatExpressionInline(rightExpr); } else { result += nodeToString(binExpr->right); } return result; } else if (auto cmdSubst = std::dynamic_pointer_cast<cmd::CommandSubstitution>(expr)) { std::string result = "<span class=\"operator\">$(</span>"; if (auto cmd = std::dynamic_pointer_cast<cmd::Command>(cmdSubst->command)) { result += "<span class=\"identifier\">" + escapeHtml(cmd->name) + "</span>"; for (size_t arg_index = 0; arg_index < cmd->args.size(); arg_index++) { const auto& arg = cmd->args[arg_index]; result += " "; if (arg.type == cmd::ARG_VARIABLE) { result += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.type == cmd::ARG_STRING_LITERAL) { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else if (cmd->name == "printf" || cmd->name == "echo" || cmd->name == "puts" || cmd->name == "print" || cmd->name == "say") { if (arg_index == 0) { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else { if (arg.type == cmd::ARG_VARIABLE) { result += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.value.find_first_of("0123456789") == 0 && arg.value.find_first_not_of("0123456789.") == std::string::npos) { result += formatNumberWithPrecision(arg.value); } else { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } } } else { if (arg.type == cmd::ARG_VARIABLE) { result += "<span class=\"variable\">" + escapeHtml(arg.value) + "</span>"; } else if (arg.value.find(' ') != std::string::npos || arg.value.find('\t') != std::string::npos || arg.value.find('\n') != std::string::npos || arg.value.find('\\') != std::string::npos || arg.value.find('%') != std::string::npos || arg.value.find('"') != std::string::npos || arg.value.find('\'') != std::string::npos) { result += "<span class=\"string\">"" + escapeHtml(formatEscapeSequences(arg.value)) + ""</span>"; } else if (arg.value.find_first_of("0123456789") == 0 && arg.value.find_first_not_of("0123456789.") == std::string::npos) { result += formatNumberWithPrecision(arg.value); } else { result += "<span class=\"identifier\">" + escapeHtml(arg.value) + "</span>"; } } } } else if (cmdSubst->command) { result += formatNodeInline(cmdSubst->command); } else { result += "<span class=\"comment\">[empty command]</span>"; } result += "<span class=\"operator\">)</span>"; return result; } else if (auto unaryExpr = std::dynamic_pointer_cast<cmd::UnaryExpression>(expr)) { std::string result; if (unaryExpr->op == cmd::UnaryExpression::INCREMENT || unaryExpr->op == cmd::UnaryExpression::DECREMENT) { std::string opStr = (unaryExpr->op == cmd::UnaryExpression::INCREMENT) ? "++" : "--"; if (unaryExpr->position == cmd::UnaryExpression::PREFIX) { result += "<span class=\"operator\">" + opStr + "</span>"; if (auto varRef = std::dynamic_pointer_cast<cmd::VariableReference>(unaryExpr->operand)) { result += "<span class=\"variable\">" + escapeHtml(varRef->name) + "</span>"; } } else { if (auto varRef = std::dynamic_pointer_cast<cmd::VariableReference>(unaryExpr->operand)) { result += "<span class=\"variable\">" + escapeHtml(varRef->name) + "</span>"; } result += "<span class=\"operator\">" + opStr + "</span>"; } } else if (unaryExpr->op == cmd::UnaryExpression::NEGATE) { if (auto numLit = std::dynamic_pointer_cast<cmd::NumberLiteral>(unaryExpr->operand)) { result += formatNumberWithPrecision(std::to_string(-numLit->value)); } else { result += "<span class=\"operator\">-</span>"; result += formatExpressionInline(std::dynamic_pointer_cast<cmd::Expression>(unaryExpr->operand)); } } return result; } return nodeToString(expr); } }; CodeFormatter::format(out, node); out << " </pre>\n"; out << " </div>\n"; out << "</body>\n"; out << "</html>\n"; } }