UNPKG

build-in-public-bot

Version:

AI-powered CLI bot for automating build-in-public tweets with code screenshots

623 lines 27.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScreenshotService = void 0; const promises_1 = __importDefault(require("fs/promises")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const canvas_1 = require("canvas"); const highlight_js_1 = __importDefault(require("highlight.js")); const axios_1 = __importDefault(require("axios")); const toml = __importStar(require("@iarna/toml")); const errors_1 = require("../utils/errors"); const logger_1 = require("../utils/logger"); const theme_loader_1 = require("./theme-loader"); const wasm_rasterizer_1 = require("./wasm-rasterizer"); class ScreenshotService { static instance; themeLoader; emojiCache = new Map(); shaderConfigs = {}; constructor() { this.themeLoader = theme_loader_1.ThemeLoader.getInstance(); this.loadShaderConfigsSync(); } loadShaderConfigsSync() { try { const possiblePaths = [ path_1.default.join(__dirname, '../config/shaders.toml'), path_1.default.join(process.cwd(), 'src/config/shaders.toml'), path_1.default.join(process.cwd(), 'dist/config/shaders.toml') ]; let configContent = ''; let configPath = ''; for (const testPath of possiblePaths) { try { configContent = fs_1.default.readFileSync(testPath, 'utf-8'); configPath = testPath; break; } catch { } } if (!configContent) { throw new Error('No shader config found in any expected location'); } this.shaderConfigs = toml.parse(configContent); logger_1.logger.debug(`Loaded ${Object.keys(this.shaderConfigs).length} shader configurations from ${configPath}`); } catch (error) { logger_1.logger.warn('Failed to load shader configurations, using defaults'); this.shaderConfigs = this.getFallbackShaderConfig(); } } getFallbackShaderConfig() { return { 'halftone': { name: 'halftone', description: 'Retro halftone dot pattern effect', scale: 8.0, intensity: 0.8, dot_size_min: 0.1, dot_size_max: 0.9 }, 'wave-gradient': { name: 'wave-gradient', description: 'Animated wave with color gradients', wave_frequency: 0.02, wave_amplitude: 30.0, colors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7'], blend_factor: 0.6 }, 'disruptor': { name: 'disruptor', description: 'Digital glitch disruption effect', noise_intensity: 0.15, displacement_factor: 5.0, color_shift_red: 25, color_shift_blue: 35, frequency: 0.1 }, 'cyberpunk': { name: 'cyberpunk', description: 'Neon cyberpunk scanlines', line_spacing: 3, line_opacity: 0.1, glow_color: '#00ff88', flicker_intensity: 0.05 }, 'matrix': { name: 'matrix', description: 'Digital rain matrix effect', rain_density: 0.02, rain_speed: 2.0, fade_factor: 0.95, color_primary: '#00ff00', color_secondary: '#008800' } }; } static getInstance() { if (!ScreenshotService.instance) { ScreenshotService.instance = new ScreenshotService(); } return ScreenshotService.instance; } async generateCodeScreenshot(code, language, config, customOptions = {}) { try { logger_1.logger.debug('Generating layered Canvas screenshot...'); const theme = await this.themeLoader.getTheme(config.theme); const fontSize = customOptions.fontSize || 16; const lineHeight = fontSize * 1.5; const outerPadding = 60; const innerPadding = 40; const windowControlsHeight = customOptions.windowControls ? 50 : 0; const lines = code.split('\n'); const highlightedCode = this.highlightCode(code, language); const maxLineLength = Math.max(...lines.map(line => line.length), 20); const charWidth = fontSize * 0.6; const codeContentWidth = maxLineLength * charWidth; const codeWindowWidth = Math.round(Math.max(400, codeContentWidth + (innerPadding * 2))); const codeWindowHeight = Math.round((lines.length * lineHeight) + (innerPadding * 2) + windowControlsHeight); const totalWidth = Math.round(codeWindowWidth + (outerPadding * 2)); const totalHeight = Math.round(codeWindowHeight + (outerPadding * 2)); const shadowOffset = 30; const codeCanvas = (0, canvas_1.createCanvas)(Math.round(codeWindowWidth + shadowOffset * 2), Math.round(codeWindowHeight + shadowOffset * 2)); const codeCtx = codeCanvas.getContext('2d'); const shadowX = shadowOffset + 5; const shadowY = shadowOffset + 8; const cornerRadius = 12; codeCtx.fillStyle = 'rgba(0, 0, 0, 0.25)'; this.drawRoundedRect(codeCtx, shadowX, shadowY, codeWindowWidth, codeWindowHeight, cornerRadius); codeCtx.fill(); try { codeCtx.filter = 'blur(8px)'; codeCtx.fillStyle = 'rgba(0, 0, 0, 0.15)'; this.drawRoundedRect(codeCtx, shadowX, shadowY, codeWindowWidth, codeWindowHeight, cornerRadius); codeCtx.fill(); codeCtx.filter = 'none'; } catch { codeCtx.fillStyle = 'rgba(0, 0, 0, 0.15)'; this.drawRoundedRect(codeCtx, shadowX, shadowY, codeWindowWidth, codeWindowHeight, cornerRadius); codeCtx.fill(); } codeCtx.fillStyle = theme.background; this.drawRoundedRect(codeCtx, shadowOffset, shadowOffset, codeWindowWidth, codeWindowHeight, cornerRadius); codeCtx.fill(); codeCtx.save(); this.drawRoundedRect(codeCtx, shadowOffset, shadowOffset, codeWindowWidth, codeWindowHeight, cornerRadius); codeCtx.clip(); let contentStartY = innerPadding + shadowOffset; if (customOptions.windowControls) { this.drawWindowControls(codeCtx, codeWindowWidth, theme, shadowOffset); contentStartY += windowControlsHeight; } codeCtx.font = `${fontSize}px "SF Mono", Monaco, Consolas, monospace`; codeCtx.textBaseline = 'top'; await this.renderCodeWithTwemoji(codeCtx, highlightedCode, theme, innerPadding + shadowOffset, contentStartY, fontSize, lineHeight); codeCtx.restore(); const finalCanvas = (0, canvas_1.createCanvas)(totalWidth, totalHeight); const finalCtx = finalCanvas.getContext('2d'); finalCtx.imageSmoothingEnabled = false; finalCtx.antialias = 'none'; if (customOptions.shader) { const shaderCanvas = (0, canvas_1.createCanvas)(totalWidth, totalHeight); const shaderCtx = shaderCanvas.getContext('2d'); if (customOptions.shader === 'cyberpunk') { shaderCtx.fillStyle = '#120458'; shaderCtx.fillRect(0, 0, totalWidth, totalHeight); } await this.applyShaderEffect(shaderCtx, customOptions.shader, totalWidth, totalHeight, theme); finalCtx.drawImage(shaderCanvas, 0, 0); } else { const gradient = finalCtx.createLinearGradient(0, 0, totalWidth, totalHeight); gradient.addColorStop(0, '#667eea'); gradient.addColorStop(1, '#764ba2'); finalCtx.fillStyle = gradient; finalCtx.fillRect(0, 0, totalWidth, totalHeight); } const codeX = Math.floor((totalWidth - (codeWindowWidth + shadowOffset * 2)) / 2); const codeY = Math.floor((totalHeight - (codeWindowHeight + shadowOffset * 2)) / 2); finalCtx.drawImage(codeCanvas, codeX, codeY); return finalCanvas.toBuffer('image/png'); } catch (error) { logger_1.logger.error('Screenshot generation failed:', error); throw new errors_1.ScreenshotError('Failed to generate screenshot', error); } } highlightCode(code, language) { try { let result; if (language === 'auto' || !language) { result = highlight_js_1.default.highlightAuto(code); } else { result = highlight_js_1.default.highlight(code, { language }); } return this.parseHighlightedCode(result.value, code); } catch (error) { return code.split('\n').map(line => ({ line, tokens: [{ text: line }] })); } } parseHighlightedCode(_html, originalCode) { const lines = originalCode.split('\n'); return lines.map(line => { const tokens = []; const words = line.split(/(\s+)/); for (const word of words) { if (this.isKeyword(word.trim())) { tokens.push({ text: word, type: 'keyword' }); } else if (this.isString(word.trim())) { tokens.push({ text: word, type: 'string' }); } else if (this.isNumber(word.trim())) { tokens.push({ text: word, type: 'number' }); } else if (this.isComment(word.trim())) { tokens.push({ text: word, type: 'comment' }); } else { tokens.push({ text: word }); } } return { line, tokens }; }); } isKeyword(word) { const keywords = ['function', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 'return', 'class', 'interface', 'type', 'import', 'export', 'async', 'await']; return keywords.includes(word); } isString(word) { return (word.startsWith('"') && word.endsWith('"')) || (word.startsWith("'") && word.endsWith("'")) || (word.startsWith('`') && word.endsWith('`')); } isNumber(word) { return /^\d+(\.\d+)?$/.test(word); } isComment(word) { return word.startsWith('//') || word.startsWith('/*') || word.startsWith('#'); } async renderTextWithTwemoji(ctx, text, x, y, color, fontSize) { const emojiRegex = /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)/gu; let currentX = x; let lastIndex = 0; let match; const matches = []; while ((match = emojiRegex.exec(text)) !== null) { matches.push({ emoji: match[0], index: match.index, length: match[0].length }); } if (matches.length === 0) { ctx.fillStyle = color; ctx.fillText(text, currentX, y); return ctx.measureText(text).width; } for (const emojiMatch of matches) { if (emojiMatch.index > lastIndex) { const beforeText = text.substring(lastIndex, emojiMatch.index); ctx.fillStyle = color; ctx.fillText(beforeText, currentX, y); currentX += ctx.measureText(beforeText).width; } try { const emojiImage = await this.loadTwemoji(emojiMatch.emoji); const emojiSize = fontSize * 0.9; const emojiY = y + (fontSize * 0.05); ctx.drawImage(emojiImage, currentX, emojiY, emojiSize, emojiSize); currentX += emojiSize + 2; } catch (error) { ctx.fillStyle = color; ctx.fillText(emojiMatch.emoji, currentX, y); currentX += ctx.measureText(emojiMatch.emoji).width; } lastIndex = emojiMatch.index + emojiMatch.length; } if (lastIndex < text.length) { const remainingText = text.substring(lastIndex); ctx.fillStyle = color; ctx.fillText(remainingText, currentX, y); currentX += ctx.measureText(remainingText).width; } return currentX - x; } async loadTwemoji(emoji) { const codePoint = this.getEmojiCodePoint(emoji); if (this.emojiCache.has(codePoint)) { return this.emojiCache.get(codePoint); } const urls = [ `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/${codePoint}.png`, `https://twemoji.maxcdn.com/v/14.0.2/72x72/${codePoint}.png`, `https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/${codePoint}.png` ]; for (const url of urls) { try { const response = await axios_1.default.get(url, { responseType: 'arraybuffer', timeout: 2000, validateStatus: (status) => status === 200 }); const image = await (0, canvas_1.loadImage)(Buffer.from(response.data)); this.emojiCache.set(codePoint, image); logger_1.logger.debug(`Successfully loaded Twemoji for ${emoji} from ${url}`); return image; } catch (error) { logger_1.logger.debug(`Failed to load Twemoji from ${url}: ${error instanceof Error ? error.message : 'Unknown error'}`); continue; } } const simplifiedCodePoint = this.getSimplifiedEmojiCodePoint(emoji); if (simplifiedCodePoint !== codePoint) { for (const url of urls) { try { const fallbackUrl = url.replace(codePoint, simplifiedCodePoint); const response = await axios_1.default.get(fallbackUrl, { responseType: 'arraybuffer', timeout: 2000, validateStatus: (status) => status === 200 }); const image = await (0, canvas_1.loadImage)(Buffer.from(response.data)); this.emojiCache.set(codePoint, image); logger_1.logger.debug(`Successfully loaded fallback Twemoji for ${emoji} (${simplifiedCodePoint})`); return image; } catch (error) { continue; } } } logger_1.logger.warn(`Failed to load Twemoji for ${emoji} (${codePoint}) from all sources`); throw new Error(`Twemoji not found for ${emoji}`); } getEmojiCodePoint(emoji) { const codePoints = [...emoji].map(char => char.codePointAt(0)?.toString(16).toLowerCase()).filter(Boolean); return codePoints.join('-'); } getSimplifiedEmojiCodePoint(emoji) { const codePoints = [...emoji] .map(char => char.codePointAt(0)) .filter((cp) => cp !== undefined) .filter(cp => { return cp !== 0xFE0F && cp !== 0xFE0E && !(cp >= 0x1F3FB && cp <= 0x1F3FF); }); return codePoints.length > 0 ? codePoints[0].toString(16).toLowerCase() : ''; } drawRoundedRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); } drawWindowControls(ctx, width, theme, offsetX = 0) { ctx.fillStyle = theme.windowControls?.background || '#2d2d2d'; ctx.fillRect(offsetX, offsetX, width, 50); const buttonY = 20 + offsetX; const buttonRadius = 8; ctx.beginPath(); ctx.arc(25 + offsetX, buttonY, buttonRadius, 0, 2 * Math.PI); ctx.fillStyle = '#ff5f57'; ctx.fill(); ctx.beginPath(); ctx.arc(55 + offsetX, buttonY, buttonRadius, 0, 2 * Math.PI); ctx.fillStyle = '#ffbd2e'; ctx.fill(); ctx.beginPath(); ctx.arc(85 + offsetX, buttonY, buttonRadius, 0, 2 * Math.PI); ctx.fillStyle = '#28ca42'; ctx.fill(); } async renderCodeWithTwemoji(ctx, highlightedCode, theme, padding, startY, fontSize, lineHeight) { const colorMap = this.themeLoader.getColorMapping(theme); let currentY = startY; for (const { tokens } of highlightedCode) { let currentX = padding; for (const token of tokens) { let color = theme.text; if (token.type) { color = colorMap[`hljs-${token.type}`] || theme.text; } currentX += await this.renderTextWithTwemoji(ctx, token.text, currentX, currentY, color, fontSize); } currentY += lineHeight; } } async applyShaderEffect(ctx, shaderName, width, height, theme) { const shaderConfig = this.shaderConfigs[shaderName]; if (!shaderConfig) { logger_1.logger.warn(`Shader '${shaderName}' not found in config`); return; } const themeColors = this.extractThemeColors(theme); const rasterizer = new wasm_rasterizer_1.WasmRasterizer(width, height, themeColors); switch (shaderName) { case 'wave-gradient': rasterizer.renderWaveGradient(); break; case 'halftone': rasterizer.renderHalftone(); break; case 'disruptor': rasterizer.renderDisruptor(); break; case 'matrix': rasterizer.renderMatrix(); break; case 'cyberpunk': this.applyShaderWithImageData(ctx, shaderName, width, height, shaderConfig); return; default: logger_1.logger.warn(`Unknown shader: ${shaderName}`); return; } const pngBuffer = await rasterizer.toPNG(); const img = await (0, canvas_1.loadImage)(pngBuffer); ctx.drawImage(img, 0, 0); } applyShaderWithImageData(ctx, shaderName, width, height, shaderConfig) { const shaderCanvas = (0, canvas_1.createCanvas)(width, height); const shaderCtx = shaderCanvas.getContext('2d'); shaderCtx.drawImage(ctx.canvas, 0, 0); const imageData = shaderCtx.getImageData(0, 0, width, height); const data = imageData.data; if (shaderName === 'cyberpunk') { this.applyCyberpunk(data, width, height, shaderConfig); } shaderCtx.putImageData(imageData, 0, 0); ctx.drawImage(shaderCanvas, 0, 0); } applyCyberpunk(data, width, height, config) { const lineSpacing = config.line_spacing || 3; const lineOpacity = config.line_opacity || 0.1; const glowColor = this.hexToRgb(config.glow_color || '#00ff88'); const flickerIntensity = config.flicker_intensity || 0.05; for (let y = 0; y < height; y++) { const isLine = y % lineSpacing === 0; const flicker = Math.random() * flickerIntensity; for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; if (isLine) { const alpha = lineOpacity + flicker; data[idx] = Math.min(255, data[idx] * (1 - alpha) + glowColor.r * alpha); data[idx + 1] = Math.min(255, data[idx + 1] * (1 - alpha) + glowColor.g * alpha); data[idx + 2] = Math.min(255, data[idx + 2] * (1 - alpha) + glowColor.b * alpha); } data[idx + 1] = Math.min(255, data[idx + 1] + 10); } } } hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : { r: 255, g: 255, b: 255 }; } extractThemeColors(theme) { if (theme.shader?.colors) { return { primary: theme.shader.colors.primary, secondary: theme.shader.colors.secondary, accent: theme.shader.colors.accent, background: theme.background }; } const themeName = theme.name?.toLowerCase() || ''; if (themeName.includes('synthwave')) { return { primary: theme.keyword || '#ff79c6', secondary: theme.function || '#50fa7b', accent: theme.string || '#f1fa8c', background: theme.background || '#2d1b69' }; } else if (themeName.includes('cyberpunk')) { return { primary: theme.keyword || '#ff0080', secondary: theme.function || '#00ff80', accent: theme.number || '#00ffff', background: theme.background || '#0a0a0a' }; } else if (themeName.includes('dracula')) { return { primary: theme.keyword || '#ff79c6', secondary: theme.variable || '#8be9fd', accent: theme.string || '#f1fa8c', background: theme.background || '#282a36' }; } else if (themeName.includes('nord')) { return { primary: theme.keyword || '#81a1c1', secondary: theme.function || '#88c0d0', accent: theme.string || '#a3be8c', background: theme.background || '#2e3440' }; } else if (themeName.includes('gruvbox')) { return { primary: theme.keyword || '#fb4934', secondary: theme.function || '#fabd2f', accent: theme.string || '#b8bb26', background: theme.background || '#282828' }; } return { primary: theme.keyword || theme.text || '#ff6b6b', secondary: theme.function || theme.variable || '#4ecdc4', accent: theme.string || theme.number || '#45b7d1', background: theme.background || '#2a2a2a' }; } async readCodeFile(filePath, lineRange) { try { const absolutePath = path_1.default.resolve(filePath); await promises_1.default.access(absolutePath); const content = await promises_1.default.readFile(absolutePath, 'utf-8'); const lines = content.split('\n'); let code = content; if (lineRange) { const [start, end] = lineRange.split('-').map(n => parseInt(n, 10)); if (!isNaN(start) && !isNaN(end)) { code = lines.slice(start - 1, end).join('\n'); } else if (!isNaN(start)) { code = lines[start - 1] || ''; } } const ext = path_1.default.extname(filePath).toLowerCase(); const languageMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.java': 'java', '.c': 'c', '.cpp': 'cpp', '.cs': 'csharp', '.go': 'go', '.rs': 'rust', '.php': 'php', '.rb': 'ruby', '.swift': 'swift', '.kt': 'kotlin', '.dart': 'dart', '.r': 'r', '.m': 'objectivec', '.scala': 'scala', '.clj': 'clojure', '.lua': 'lua', '.pl': 'perl', '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell', '.ps1': 'powershell', '.yaml': 'yaml', '.yml': 'yaml', '.json': 'json', '.xml': 'xml', '.html': 'html', '.css': 'css', '.scss': 'scss', '.sass': 'sass', '.less': 'less', '.sql': 'sql', '.md': 'markdown', '.mdx': 'markdown', '.vue': 'vue', '.svelte': 'svelte' }; const language = languageMap[ext] || 'text'; return { code, language }; } catch (error) { if (error.code === 'ENOENT') { throw new errors_1.FileError(`File not found: ${filePath}`); } throw new errors_1.FileError(`Failed to read code file: ${error.message}`, error); } } async saveScreenshot(buffer) { try { const tempDir = path_1.default.join(process.cwd(), '.bip-temp'); await promises_1.default.mkdir(tempDir, { recursive: true }); const filename = `screenshot-${Date.now()}.png`; const filepath = path_1.default.join(tempDir, filename); await promises_1.default.writeFile(filepath, buffer); return filepath; } catch (error) { throw new errors_1.ScreenshotError('Failed to save screenshot', error); } } getAvailableThemes() { return this.themeLoader.getAllThemes(); } } exports.ScreenshotService = ScreenshotService; //# sourceMappingURL=screenshot.js.map