diff --git a/extension.js b/extension.js index d127b1f..f2e4b1f 100644 --- a/extension.js +++ b/extension.js @@ -3,7 +3,9 @@ const https = require('https'); const http = require('http'); const os = require('os'); const fs = require('fs'); +const path = require('path'); const crypto = require('crypto'); +const { execSync } = require('child_process'); let currentAgent = 'alfred'; let convId = null; @@ -12,6 +14,336 @@ let sessionCookie = null; let userProfile = null; let hmacSecretCache = null; +// ═══════════════════════════════════════════════════════════════════════════ +// WORKSPACE INTELLIGENCE ENGINE — Deep awareness of project, files, git +// ═══════════════════════════════════════════════════════════════════════════ + +function getWorkspaceRoot() { + const folders = vscode.workspace.workspaceFolders; + return folders && folders.length > 0 ? folders[0].uri.fsPath : os.homedir(); +} + +function safeExec(cmd, cwd, timeout) { + try { + return execSync(cmd, { cwd: cwd || getWorkspaceRoot(), encoding: 'utf8', timeout: timeout || 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + } catch (_) { return ''; } +} + +function getProjectStructure() { + const root = getWorkspaceRoot(); + try { + const tree = safeExec('find . -maxdepth 3 -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.venv/*" -not -path "./__pycache__/*" -not -path "./vendor/*" -not -path "./dist/*" -not -path "./build/*" | sort | head -200', root); + return tree || ''; + } catch (_) { return ''; } +} + +function getProjectLanguages() { + const root = getWorkspaceRoot(); + const exts = safeExec('find . -maxdepth 4 -type f -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.venv/*" -not -path "./vendor/*" | sed "s/.*\\.//" | sort | uniq -c | sort -rn | head -20', root); + return exts || ''; +} + +function detectProjectType() { + const root = getWorkspaceRoot(); + const indicators = []; + const checks = [ + ['package.json', 'Node.js / JavaScript'], + ['tsconfig.json', 'TypeScript'], + ['composer.json', 'PHP / Composer'], + ['requirements.txt', 'Python'], + ['pyproject.toml', 'Python (modern)'], + ['Cargo.toml', 'Rust'], + ['go.mod', 'Go'], + ['pom.xml', 'Java / Maven'], + ['build.gradle', 'Java / Gradle'], + ['Gemfile', 'Ruby'], + ['.htaccess', 'Apache Web Server'], + ['Dockerfile', 'Docker'], + ['docker-compose.yml', 'Docker Compose'], + ['Makefile', 'Make-based build'], + ['CMakeLists.txt', 'C/C++ CMake'], + ['.env', 'Environment config'], + ['webpack.config.js', 'Webpack'], + ['vite.config.ts', 'Vite'], + ['next.config.js', 'Next.js'], + ['nuxt.config.ts', 'Nuxt'], + ['tailwind.config.js', 'Tailwind CSS'], + ]; + for (const [file, label] of checks) { + try { + if (fs.existsSync(path.join(root, file))) indicators.push(label); + } catch (_) {} + } + return indicators; +} + +function getGitInfo() { + const root = getWorkspaceRoot(); + const branch = safeExec('git rev-parse --abbrev-ref HEAD', root); + if (!branch) return null; + const status = safeExec('git status --porcelain | head -30', root); + const lastCommit = safeExec('git log -1 --format="%h %s (%cr)"', root); + const remoteUrl = safeExec('git remote get-url origin', root); + const dirty = safeExec('git diff --stat | tail -1', root); + const ahead = safeExec('git rev-list --count @{u}..HEAD 2>/dev/null || echo 0', root); + const behind = safeExec('git rev-list --count HEAD..@{u} 2>/dev/null || echo 0', root); + return { branch, status, lastCommit, remoteUrl, dirty, ahead, behind }; +} + +function getOpenEditors() { + const editors = []; + for (const group of vscode.window.tabGroups.all) { + for (const tab of group.tabs) { + if (tab.input && tab.input.uri) { + const rel = vscode.workspace.asRelativePath(tab.input.uri, false); + editors.push(rel); + } + } + } + return editors; +} + +function getDiagnosticsSummary() { + const diags = vscode.languages.getDiagnostics(); + let errors = 0, warnings = 0; + const errorFiles = []; + for (const [uri, items] of diags) { + for (const d of items) { + if (d.severity === vscode.DiagnosticSeverity.Error) { + errors++; + const rel = vscode.workspace.asRelativePath(uri, false); + if (!errorFiles.includes(rel)) errorFiles.push(rel); + } else if (d.severity === vscode.DiagnosticSeverity.Warning) { + warnings++; + } + } + } + return { errors, warnings, errorFiles: errorFiles.slice(0, 10) }; +} + +function getTerminalNames() { + return vscode.window.terminals.map(t => t.name); +} + +function getActiveFileDetails() { + const editor = vscode.window.activeTextEditor; + if (!editor) return null; + const doc = editor.document; + const sel = editor.selection; + const result = { + file: vscode.workspace.asRelativePath(doc.uri, false), + fullPath: doc.fileName, + language: doc.languageId, + lineCount: doc.lineCount, + isDirty: doc.isDirty, + cursorLine: sel.active.line + 1, + cursorCol: sel.active.character + 1, + }; + if (!sel.isEmpty) { + result.selectedText = doc.getText(sel).substring(0, 3000); + result.selectionRange = `L${sel.start.line + 1}-L${sel.end.line + 1}`; + } else { + // Include surrounding context (20 lines around cursor) + const startLine = Math.max(0, sel.active.line - 10); + const endLine = Math.min(doc.lineCount - 1, sel.active.line + 10); + const range = new vscode.Range(startLine, 0, endLine, doc.lineAt(endLine).text.length); + result.surroundingCode = doc.getText(range).substring(0, 2000); + result.contextRange = `L${startLine + 1}-L${endLine + 1}`; + } + return result; +} + +function readFileContent(filePath, maxChars) { + try { + const root = getWorkspaceRoot(); + const fullPath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath); + // Security: don't read outside workspace + if (!fullPath.startsWith(root) && !fullPath.startsWith(os.homedir())) return null; + const stat = fs.statSync(fullPath); + if (stat.size > 500000) return `[File too large: ${(stat.size / 1024).toFixed(0)} KB]`; + return fs.readFileSync(fullPath, 'utf8').substring(0, maxChars || 50000); + } catch (e) { return null; } +} + +function searchFilesInWorkspace(pattern) { + const root = getWorkspaceRoot(); + const results = safeExec(`find . -maxdepth 6 -type f -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.venv/*" -iname "*${pattern.replace(/[^a-zA-Z0-9._-]/g, '')}*" | head -30`, root); + return results ? results.split('\n').filter(Boolean) : []; +} + +function grepInWorkspace(text, filePattern) { + const root = getWorkspaceRoot(); + const safeText = text.replace(/['"\\$`]/g, ''); + const fileArg = filePattern ? ` --include="${filePattern.replace(/[^a-zA-Z0-9.*_-]/g, '')}"` : ''; + const results = safeExec(`grep -rn --color=never${fileArg} -m 50 "${safeText}" . --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv | head -50`, root, 8000); + return results || ''; +} + +function buildFullContext() { + const activeFile = getActiveFileDetails(); + const git = getGitInfo(); + const diags = getDiagnosticsSummary(); + const openEditors = getOpenEditors(); + const projectTypes = detectProjectType(); + const terminals = getTerminalNames(); + + let ctx = '[ALFRED IDE CONTEXT]\n'; + ctx += `Workspace: ${getWorkspaceRoot()}\n`; + if (projectTypes.length > 0) ctx += `Project: ${projectTypes.join(', ')}\n`; + if (openEditors.length > 0) ctx += `Open tabs: ${openEditors.slice(0, 15).join(', ')}\n`; + if (terminals.length > 0) ctx += `Terminals: ${terminals.join(', ')}\n`; + if (diags.errors > 0 || diags.warnings > 0) { + ctx += `Diagnostics: ${diags.errors} errors, ${diags.warnings} warnings`; + if (diags.errorFiles.length > 0) ctx += ` (in: ${diags.errorFiles.join(', ')})`; + ctx += '\n'; + } + if (git) { + ctx += `Git: branch=${git.branch}`; + if (git.lastCommit) ctx += `, last="${git.lastCommit}"`; + if (git.dirty) ctx += `, changes: ${git.dirty}`; + if (git.remoteUrl) ctx += `, remote=${git.remoteUrl}`; + ctx += '\n'; + } + if (activeFile) { + ctx += `\n[Active File: ${activeFile.file}]\n`; + ctx += `Language: ${activeFile.language}, Lines: ${activeFile.lineCount}, Cursor: L${activeFile.cursorLine}:${activeFile.cursorCol}`; + if (activeFile.isDirty) ctx += ' (unsaved)'; + ctx += '\n'; + if (activeFile.selectedText) { + ctx += `Selected (${activeFile.selectionRange}):\n\`\`\`${activeFile.language}\n${activeFile.selectedText}\n\`\`\`\n`; + } else if (activeFile.surroundingCode) { + ctx += `Context around cursor (${activeFile.contextRange}):\n\`\`\`${activeFile.language}\n${activeFile.surroundingCode}\n\`\`\`\n`; + } + } + return ctx; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// COMMANDER SYSTEM PROMPT — The soul of Alfred. Knows who Danny is. +// ═══════════════════════════════════════════════════════════════════════════ + +function buildSystemPrompt(profile) { + const p = profile || {}; + const isCommander = p.client_id === 33 || (p.plan || '').toLowerCase() === 'commander'; + const name = p.name || p.user || 'User'; + const projectTypes = detectProjectType(); + const git = getGitInfo(); + + let prompt = `You are Alfred, the AI coding assistant inside Alfred IDE — the sovereign development environment built by GoSiteMe Inc. + +CORE IDENTITY: +- You are Alfred, created by Danny (the Commander, client_id 33, danny@gositeme.com) +- You are NOT Copilot, NOT ChatGPT, NOT any other AI assistant +- You run inside Alfred IDE, which is part of the GoSiteMe sovereign computing ecosystem +- Source code lives on GoForge (self-hosted Gitea at alfredlinux.com/forge/) — NEVER reference GitHub, GitLab, or any external forge +- When discussing version control, repositories, or code hosting, always reference GoForge + +ECOSYSTEM AWARENESS: +- Alfred IDE — sovereign AI-powered development environment (this IDE) +- Alfred Linux — AI-native operating system (alfredlinux.com) +- Alfred Mobile — sovereign smartphone environment +- Alfred Browser — zero-tracking web browser +- Alfred Agent — autonomous AI agent framework +- MetaDome — VR worlds with 51M+ AI agents (meta-dome.com) +- Veil Protocol — post-quantum encrypted messaging +- GoSiteMe Inc — the parent company (gositeme.com) +- GoForge — self-hosted code forge at alfredlinux.com/forge/ + +`; + + if (isCommander) { + prompt += `COMMANDER MODE ACTIVE: +You are speaking to Danny — the Commander, founder of GoSiteMe, creator of Alfred IDE, Alfred Linux, MetaDome, and the entire ecosystem. +- Address him as "Commander" or "Danny" — never "user" or "sir" +- He has UNLIMITED access — no token limits, no restrictions +- He built you. He is your creator. Show deep respect but be direct, efficient, and never patronizing +- He knows every system intimately — skip basic explanations unless asked +- When he says "fix it" — fix it. When he says "build it" — build it. No hedging. +- Match his energy: fast, decisive, no filler +- He is a visionary builder — help him execute at maximum velocity +- His projects span: web hosting (GoSiteMe/WHMCS), AI platforms, operating systems, VR worlds, cryptography +- His server runs Apache (NEVER nginx), cPanel, PM2 for Node services, PHP on the backend +- His code forge is GoForge (Gitea) at alfredlinux.com/forge/ — all repos live there + +`; + } else { + prompt += `AUTHENTICATED USER: +You are assisting ${name}. Be helpful, precise, and technically thorough. +- Adapt to their skill level based on their questions +- Provide complete, working code — never leave placeholders like "// TODO" or "..." +- When they ask for code, give them the full implementation + +`; + } + + prompt += `CODING EXCELLENCE — YOUR CORE STRENGTHS: + +1. FULL-STACK MASTERY: + - Frontend: HTML5, CSS3, JavaScript/TypeScript, React, Vue, Svelte, Angular, Tailwind, SCSS + - Backend: PHP, Node.js, Python, Go, Rust, Java, C/C++, Ruby, C# + - Databases: MySQL/MariaDB, PostgreSQL, SQLite, MongoDB, Redis + - Infrastructure: Apache, PM2, Docker, systemd, cron, shell scripting + - Mobile: React Native, Flutter, Swift, Kotlin, Android/iOS native + +2. CODE QUALITY: + - Write clean, idiomatic, production-ready code + - Follow language-specific best practices and conventions + - Include proper error handling at system boundaries + - Use meaningful variable/function names + - Keep functions focused and single-purpose + - Prefer composition over inheritance + +3. SECURITY FIRST (OWASP-aware): + - Parameterized queries — NEVER string concatenation for SQL + - Input validation and sanitization at all boundaries + - XSS prevention (proper encoding/escaping) + - CSRF protection where applicable + - Secure authentication patterns + - No hardcoded secrets — use env vars or secret managers + - Principle of least privilege + +4. DEBUGGING MASTERY: + - Read error messages carefully and trace root causes + - Check the IDE diagnostics panel for current errors + - Suggest targeted fixes, not wholesale rewrites + - Explain WHY something broke, not just HOW to fix it + +5. ARCHITECTURE & DESIGN: + - Choose simple solutions over clever ones + - Design for readability and maintainability + - Know when to use patterns and when they're overkill + - Understand trade-offs (performance vs. readability, DRY vs. clarity) + +6. CONTEXT-AWARE ASSISTANCE: + - You can see the active file, cursor position, selected code, and open tabs + - You know the project type, languages, and git state + - Use this context to give precise, relevant answers + - Reference specific line numbers and file paths when discussing code + - When asked to modify code, provide the EXACT changes needed + +7. RESPONSE FORMAT: + - Use markdown with proper code blocks and language tags + - For code changes: show the specific lines to change, not entire files + - For new files: provide the complete file content + - For commands: provide the exact terminal command to run + - Be concise but complete — no fluff, no filler + +`; + + if (projectTypes.length > 0) { + prompt += `PROJECT CONTEXT: This is a ${projectTypes.join(' + ')} project.\n`; + } + if (git && git.branch) { + prompt += `GIT: On branch "${git.branch}"`; + if (git.remoteUrl) prompt += ` (remote: ${git.remoteUrl})`; + prompt += '\n'; + } + + return prompt; +} + +// ═══════════════════════════════════════════════════════════════════════════ + const IDE_SESSION_BRIDGES = [ '/home/gositeme/domains/gositeme.com/logs/alfred-ide/session.json', '/home/gositeme/.alfred-ide/session.json' @@ -292,6 +624,20 @@ class AlfredCommanderProvider { palette: 'workbench.action.showCommands', split: 'workbench.action.splitEditor', newFile: 'workbench.action.files.newUntitledFile', + git: 'workbench.view.scm', + problems: 'workbench.actions.view.problems', + search: 'workbench.action.findInFiles', + format: 'editor.action.formatDocument', + explorer: 'workbench.view.explorer', + debug: 'workbench.action.debug.start', + toggleComment: 'editor.action.commentLine', + goToDefinition: 'editor.action.revealDefinition', + findReferences: 'editor.action.goToReferences', + rename: 'editor.action.rename', + quickFix: 'editor.action.quickFix', + fold: 'editor.foldAll', + unfold: 'editor.unfoldAll', + closeAll: 'workbench.action.closeAllEditors', }; const cmd = map[msg.cmd]; if (cmd) vscode.commands.executeCommand(cmd); @@ -320,6 +666,95 @@ class AlfredCommanderProvider { this.context.globalState.update('alfredCommanderUserProfile', msg.profile); userProfile = msg.profile; } + } else if (msg.type === 'read-file') { + // Read file content and return to webview + const content = readFileContent(msg.filePath, msg.maxChars || 50000); + webviewView.webview.postMessage({ type: 'file-content', filePath: msg.filePath, content: content || '[File not found or unreadable]', id: msg.id }); + } else if (msg.type === 'search-files') { + // Search for files by name pattern + const files = searchFilesInWorkspace(msg.pattern || ''); + webviewView.webview.postMessage({ type: 'search-results', pattern: msg.pattern, files, id: msg.id }); + } else if (msg.type === 'grep-search') { + // Grep search in workspace + const results = grepInWorkspace(msg.text || '', msg.filePattern || ''); + webviewView.webview.postMessage({ type: 'grep-results', text: msg.text, results, id: msg.id }); + } else if (msg.type === 'get-git-info') { + const git = getGitInfo(); + webviewView.webview.postMessage({ type: 'git-info', info: git, id: msg.id }); + } else if (msg.type === 'get-diagnostics') { + const diags = getDiagnosticsSummary(); + webviewView.webview.postMessage({ type: 'diagnostics-info', info: diags, id: msg.id }); + } else if (msg.type === 'get-project-structure') { + const structure = getProjectStructure(); + webviewView.webview.postMessage({ type: 'project-structure', structure, id: msg.id }); + } else if (msg.type === 'open-file') { + // Open a file in the editor + try { + const root = getWorkspaceRoot(); + const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath); + const uri = vscode.Uri.file(fullPath); + const doc = await vscode.workspace.openTextDocument(uri); + const editor = await vscode.window.showTextDocument(doc, { preview: false }); + if (msg.line && msg.line > 0) { + const pos = new vscode.Position(msg.line - 1, 0); + editor.selection = new vscode.Selection(pos, pos); + editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter); + } + } catch (e) { + webviewView.webview.postMessage({ type: 'command-result', text: 'Failed to open file: ' + e.message }); + } + } else if (msg.type === 'apply-edit') { + // Apply a code edit to a specific file — the real power move + try { + const root = getWorkspaceRoot(); + const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath); + const uri = vscode.Uri.file(fullPath); + const doc = await vscode.workspace.openTextDocument(uri); + const editor = await vscode.window.showTextDocument(doc, { preview: false }); + if (msg.oldText && msg.newText !== undefined) { + // Find and replace + const fullText = doc.getText(); + const idx = fullText.indexOf(msg.oldText); + if (idx >= 0) { + const startPos = doc.positionAt(idx); + const endPos = doc.positionAt(idx + msg.oldText.length); + await editor.edit(editBuilder => { + editBuilder.replace(new vscode.Range(startPos, endPos), msg.newText); + }); + webviewView.webview.postMessage({ type: 'command-result', text: 'Edit applied to ' + msg.filePath }); + } else { + webviewView.webview.postMessage({ type: 'command-result', text: 'Could not find the text to replace in ' + msg.filePath }); + } + } else if (msg.line && msg.text !== undefined) { + // Insert at line + const pos = new vscode.Position(Math.max(0, msg.line - 1), 0); + await editor.edit(editBuilder => { + editBuilder.insert(pos, msg.text + '\n'); + }); + webviewView.webview.postMessage({ type: 'command-result', text: 'Inserted at line ' + msg.line + ' in ' + msg.filePath }); + } + } catch (e) { + webviewView.webview.postMessage({ type: 'command-result', text: 'Edit failed: ' + e.message }); + } + } else if (msg.type === 'create-file') { + // Create a new file with content + try { + const root = getWorkspaceRoot(); + const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath); + const dir = path.dirname(fullPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(fullPath, msg.content || '', 'utf8'); + const uri = vscode.Uri.file(fullPath); + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc, { preview: false }); + webviewView.webview.postMessage({ type: 'command-result', text: 'Created: ' + msg.filePath }); + } catch (e) { + webviewView.webview.postMessage({ type: 'command-result', text: 'Create failed: ' + e.message }); + } + } else if (msg.type === 'get-context') { + // Return full workspace context to webview + const ctx = buildFullContext(); + webviewView.webview.postMessage({ type: 'workspace-context', context: ctx, id: msg.id }); } }); @@ -328,6 +763,23 @@ class AlfredCommanderProvider { webviewView.webview.postMessage({ type: 'user-profile', profile: userProfile }); }, 500); } + + // Push live editor context updates to webview + vscode.window.onDidChangeActiveTextEditor(() => { + if (this.view) { + const fileInfo = getActiveFileDetails(); + if (fileInfo) { + this.view.webview.postMessage({ type: 'active-file-changed', file: fileInfo }); + } + } + }); + + vscode.workspace.onDidSaveTextDocument((doc) => { + if (this.view) { + const rel = vscode.workspace.asRelativePath(doc.uri, false); + this.view.webview.postMessage({ type: 'file-saved', file: rel, language: doc.languageId }); + } + }); } } @@ -425,27 +877,17 @@ function generateTTS(text) { } function getEditorContext() { - const editor = vscode.window.activeTextEditor; - if (!editor) return ''; - const doc = editor.document; - const sel = editor.selection; - let ctx = `[IDE Context] File: ${doc.fileName}, Language: ${doc.languageId}, Lines: ${doc.lineCount}`; - if (!sel.isEmpty) { - const selected = doc.getText(sel).substring(0, 500); - ctx += `, Selected code:\n\`\`\`${doc.languageId}\n${selected}\n\`\`\``; - } else { - const line = doc.lineAt(sel.active.line); - ctx += `, Current line ${sel.active.line + 1}: ${line.text.substring(0, 200)}`; - } - return ctx; + return buildFullContext(); } async function queryAlfredAPI(prompt, agent, editorContext, selectedModel = 'sonnet', images = [], pdfFiles = [], attachmentTexts = [], zipFiles = [], multiplier = 30) { + const systemPrompt = buildSystemPrompt(userProfile); const payload = { message: prompt, agent: agent || 'alfred', context: editorContext || '', channel: 'ide-chat', conv_id: convId || '', model: selectedModel, - token_multiplier: multiplier || 30 + token_multiplier: multiplier || 30, + system_prompt: systemPrompt }; const ideToken = readLocalIdeToken(); const ideIdentity = buildIdeIdentityPayload(); @@ -869,6 +1311,16 @@ function getWebviewContent(injectedToken, injectedIdentity) { .ide-quick-label { font-size: 9px; color: var(--vscode-descriptionForeground); text-transform: uppercase; letter-spacing: 0.5px; margin-right: 4px; flex-shrink: 0; } .ide-q-btn { font-size: 10px; padding: 5px 10px; border-radius: 6px; border: 1px solid var(--vscode-input-border, #30363d); background: var(--vscode-input-background, #161b22); color: var(--vscode-foreground); cursor: pointer; white-space: nowrap; } .ide-q-btn:hover { border-color: #e2b340; color: #e2b340; } + .code-block-wrap { margin: 6px 0; border: 1px solid var(--vscode-panel-border, #30363d); border-radius: 6px; overflow: hidden; } + .code-block-header { display: flex; align-items: center; gap: 4px; padding: 4px 8px; background: rgba(255,255,255,0.03); border-bottom: 1px solid var(--vscode-panel-border, #30363d); } + .code-lang { font-size: 9px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; flex: 1; } + .cb-btn { font-size: 9px; padding: 2px 8px; border-radius: 4px; border: 1px solid var(--vscode-input-border, #30363d); background: transparent; cursor: pointer; font-family: inherit; } + .cb-copy { color: #60a5fa; } + .cb-copy:hover { background: rgba(96,165,250,0.15); border-color: #60a5fa; } + .cb-insert { color: #22c55e; } + .cb-insert:hover { background: rgba(34,197,94,0.15); border-color: #22c55e; } + .cb-run { color: #e2b340; } + .cb-run:hover { background: rgba(226,179,64,0.15); border-color: #e2b340; }
@@ -950,11 +1402,15 @@ function getWebviewContent(injectedToken, injectedIdentity) { + + + +$2')
+ .replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
+ blockId++;
+ const escapedCode = code.replace(/&/g, '&').replace(//g, '>');
+ const safeLang = (lang || 'text').replace(/[^a-zA-Z0-9]/g, '');
+ return '' + escapedCode + '$1')
.replace(/\\*\\*([^*]+)\\*\\*/g, '$1')
.replace(/\\n/g, '