build-in-public-bot
Version:
AI-powered CLI bot for automating build-in-public tweets with code screenshots
623 lines • 27.6 kB
JavaScript
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
;