From 410104ecf47136f89a17b1e781fe8fc9d3152324 Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:06:15 -0800 Subject: [PATCH 1/6] chore: test improvements --- .github/workflows/ci.yaml | 11 +- README.md | 26 +-- codecov.yml | 36 ++++ package.json | 1 + scripts/remove-comments.ts | 97 +++++++++++ src/cli/commands/theme.ts | 18 +- src/cli/interactive.ts | 6 +- src/cli/theme/generator.ts | 16 +- src/cli/theme/transporter.ts | 18 +- src/cli/types.ts | 4 +- src/cli/utils.ts | 38 ++--- src/index.ts | 178 ++++++++++---------- src/renderer/constants.ts | 30 ++-- src/renderer/detectBackground.ts | 66 ++++---- src/renderer/index.ts | 162 +++++++++--------- src/renderer/lightBox.ts | 58 +++---- src/schema/validator.ts | 20 +-- src/themes/color-constants.ts | 6 +- src/themes/index.ts | 34 ++-- src/themes/template.ts | 4 +- src/themes/utils.ts | 44 ++--- src/tokenizer/index.ts | 34 ++-- src/types.ts | 40 ++--- src/utils/ascii.ts | 4 +- src/utils/boxen.ts | 14 +- src/utils/colors.ts | 12 +- src/utils/gradient.ts | 2 +- src/utils/spinner.ts | 12 +- tests/unit/cli/index.test.ts | 2 +- tests/unit/cli/theme/generator.test.ts | 8 +- tests/unit/cli/theme/transporter.test.ts | 22 +-- tests/unit/index.test.ts | 52 +++--- tests/unit/renderer/constants.test.ts | 2 +- tests/unit/renderer/index.test.ts | 20 +-- tests/unit/schema/validator.test.ts | 16 +- tests/unit/themes/index.test.ts | 10 +- tests/unit/themes/template.test.ts | 10 +- tests/unit/themes/utils.test.ts | 8 +- tests/unit/tokenizer/index.test.ts | 200 ++++++++++++++++++++--- tests/unit/utils/logger.test.ts | 2 +- tests/unit/utils/spinner.test.ts | 6 +- 41 files changed, 821 insertions(+), 528 deletions(-) create mode 100644 codecov.yml create mode 100644 scripts/remove-comments.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 031b37e..c13000d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,17 +25,18 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Run tests - run: bun test - - name: Run tests with coverage - run: bun test --coverage + run: bun test --coverage --coverage-reporter=lcov - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + files: ./coverage/lcov.info + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + verbose: true - name: Run linting (OxLint) run: bun run lint diff --git a/README.md b/README.md index 18f706d..95f0410 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ LogsDX is a schema-based theming engine that applies consistent visual styling t ## Features -- 🎨 **Unified Theming** - One theme works in terminal, browser, and CI/CD -- 🚀 **Zero Dependencies** - Works with any existing logger -- 📦 **8+ Built-in Themes** - Production-ready themes included -- 🛠️ **CLI Tool** - Process log files with beautiful formatting -- ♿ **Accessible** - WCAG compliance checking built-in -- 🌓 **Light/Dark Mode** - Automatic theme adaptation +- **Unified Theming** - One theme works in terminal, browser, and CI/CD +- **Zero Dependencies** - Works with any existing logger +- **8+ Built-in Themes** - Production-ready themes included +- **CLI Tool** - Process log files with beautiful formatting +- **Accessible** - WCAG compliance checking built-in +- **Light/Dark Mode** - Automatic theme adaptation ## Installation @@ -514,10 +514,10 @@ bun run format # Format code The setup script will: -- ✅ Install all dependencies -- ✅ Build the main package -- ✅ Configure git hooks (pre-commit, post-merge, post-checkout) -- ✅ Verify your environment is ready +- Install all dependencies +- Build the main package +- Configure git hooks (pre-commit, post-merge, post-checkout) +- Verify your environment is ready ## License @@ -525,6 +525,6 @@ MIT © LogsDX Contributors ## Support -- 📘 [Documentation](https://jeffry.in/logsdx) -- 🐛 [Report Issues](https://github.com/yowainwright/logsdx/issues) -- 💬 [Discussions](https://github.com/yowainwright/logsdx/discussions) +- [Documentation](https://jeffry.in/logsdx) +- [Report Issues](https://github.com/yowainwright/logsdx/issues) +- [Discussions](https://github.com/yowainwright/logsdx/discussions) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..0be4076 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,36 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "85...100" + + status: + project: + default: + target: 95% + threshold: 1% + if_ci_failed: error + + patch: + default: + target: 90% + threshold: 5% + +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: false + require_base: false + require_head: true + +ignore: + - "tests/**/*" + - "**/*.test.ts" + - "**/*.config.ts" + - "**/*.config.js" + - "scripts/**/*" + - "site/**/*" + - "examples/**/*" + - "dist/**/*" diff --git a/package.json b/package.json index 6c9b8a4..36403c4 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "build:cli": "bun build src/cli/bin.ts --outfile dist/cli.js --format cjs --minify --target node", "build:types": "tsc -p tsconfig.build.json", "test": "bun test", + "test:coverage": "bun test --coverage --coverage-reporter=lcov", "test:watch": "bun test --watch", "lint": "oxlint .", "lint:fix": "oxlint . --fix", diff --git a/scripts/remove-comments.ts b/scripts/remove-comments.ts new file mode 100644 index 0000000..157b26e --- /dev/null +++ b/scripts/remove-comments.ts @@ -0,0 +1,97 @@ +#!/usr/bin/env bun + +import { readdir, readFile, writeFile } from "fs/promises"; +import { join } from "path"; + +async function getAllTsFiles(dir: string): Promise { + const files: string[] = []; + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await getAllTsFiles(fullPath))); + } else if (entry.name.endsWith(".ts")) { + files.push(fullPath); + } + } + + return files; +} + +function removeComments(code: string): string { + let result = ""; + let i = 0; + let inString = false; + let stringChar = ""; + let inTemplate = false; + + while (i < code.length) { + const char = code[i]; + const nextChar = code[i + 1]; + + if (!inString && !inTemplate && char === "/" && nextChar === "/") { + const lineEnd = code.indexOf("\n", i); + if (lineEnd === -1) { + break; + } + result += "\n"; + i = lineEnd + 1; + continue; + } + + if (!inString && !inTemplate && char === "/" && nextChar === "*") { + const commentEnd = code.indexOf("*/", i + 2); + if (commentEnd === -1) { + break; + } + const commentContent = code.substring(i, commentEnd + 2); + const newlines = (commentContent.match(/\n/g) || []).length; + result += "\n".repeat(newlines); + i = commentEnd + 2; + continue; + } + + if (!inTemplate && (char === '"' || char === "'")) { + if (!inString) { + inString = true; + stringChar = char; + } else if (char === stringChar && code[i - 1] !== "\\") { + inString = false; + stringChar = ""; + } + } + + if (!inString && char === "`") { + inTemplate = !inTemplate; + } + + result += char; + i++; + } + + return result; +} + +async function processFile(filePath: string) { + console.log(`Processing: ${filePath}`); + const content = await readFile(filePath, "utf-8"); + const cleaned = removeComments(content); + await writeFile(filePath, cleaned, "utf-8"); +} + +async function main() { + const srcFiles = await getAllTsFiles("src"); + const testFiles = await getAllTsFiles("tests"); + const allFiles = [...srcFiles, ...testFiles]; + + console.log(`Found ${allFiles.length} TypeScript files`); + + for (const file of allFiles) { + await processFile(file); + } + + console.log("Done!"); +} + +main().catch(console.error); diff --git a/src/cli/commands/theme.ts b/src/cli/commands/theme.ts index 479db1f..bc966e7 100644 --- a/src/cli/commands/theme.ts +++ b/src/cli/commands/theme.ts @@ -16,7 +16,7 @@ import { registerTheme, getTheme, getThemeNames } from "../../themes"; import { getLogsDX } from "../../index"; import { Theme } from "../../types"; -// Sample logs for preview + const SAMPLE_LOGS = [ "INFO: Server started on port 3000", "WARN: Memory usage high: 85%", @@ -28,7 +28,7 @@ const SAMPLE_LOGS = [ "Cache hit ratio: 92.5%", ]; -// Color presets + const COLOR_PRESETS = { Vibrant: { primary: "#007acc", @@ -105,7 +105,7 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { showBanner(); } - // Basic info + const name = await input({ message: "Theme name:", validate: (inputValue: string) => { @@ -138,7 +138,7 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { mode, }; - // Color selection + const preset = await select({ message: "Choose a color preset:", choices: Object.keys(COLOR_PRESETS).map((name) => ({ @@ -211,7 +211,7 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { colors = { primary, error, warning, success, info, muted }; } - // Feature presets + const presets = await checkbox({ message: "Select features to highlight:", choices: [ @@ -236,7 +236,7 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { ], }); - // Create theme + const createSpinner = spinner("Creating theme...").start(); const config: SimpleThemeConfig = { @@ -250,11 +250,11 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { const theme = createTheme(config); createSpinner.succeed("Theme created!"); - // Preview + console.log("\n"); renderPreview(theme, `✨ ${theme.name} Preview`); - // Accessibility check + const checkAccessibility = await confirm({ message: "Check accessibility compliance?", default: true, @@ -301,7 +301,7 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { } } - // Save options + const saveOption = await select({ message: "How would you like to save the theme?", choices: [ diff --git a/src/cli/interactive.ts b/src/cli/interactive.ts index ec98c2e..ec356aa 100644 --- a/src/cli/interactive.ts +++ b/src/cli/interactive.ts @@ -39,7 +39,7 @@ export async function runInteractiveMode(): Promise { ), ); - // Theme selection + const themeNames = getThemeNames(); const themeChoices: ThemeChoice[] = themeNames.map((name: string) => ({ name: chalk.cyan(name), @@ -79,7 +79,7 @@ export async function runInteractiveMode(): Promise { }); } - // Output format selection + const outputFormat = await select({ message: "📤 Choose output format:", choices: [ @@ -96,7 +96,7 @@ export async function runInteractiveMode(): Promise { ], }); - // Preview confirmation + const wantPreview = await confirm({ message: "👀 Show a preview with your settings?", default: true, diff --git a/src/cli/theme/generator.ts b/src/cli/theme/generator.ts index 09b86b6..36ee592 100644 --- a/src/cli/theme/generator.ts +++ b/src/cli/theme/generator.ts @@ -391,21 +391,21 @@ export function validateColorInput(color: string): boolean | string { return false; } - // Check hex pattern - must start with # + if (color.match(/^[0-9a-fA-F]+$/)) { - return false; // Hex without # is invalid + return false; } if (color.startsWith("#")) { return hexPattern.test(color); } - // Check RGB pattern + if (color.startsWith("rgb")) { return rgbPattern.test(color); } - // Check named colors + return namedColors.includes(color.toLowerCase()); } @@ -456,7 +456,7 @@ interface ThemeAnswers { } export function generateTemplateFromAnswers(answers: ThemeAnswers): Theme { - // Map features to pattern presets + const patternPresets = answers.patterns || answers.patternPresets || []; if (answers.features && answers.features.includes("logLevels")) { patternPresets.push("log-levels"); @@ -478,12 +478,12 @@ export function generateTemplateFromAnswers(answers: ThemeAnswers): Theme { theme.mode = answers.mode as Theme["mode"]; } - // Ensure schema exists + if (!theme.schema) { theme.schema = {}; } - // Add features that aren't in pattern presets + if (answers.features) { if ( answers.features.includes("numbers") || @@ -495,7 +495,7 @@ export function generateTemplateFromAnswers(answers: ThemeAnswers): Theme { theme.schema.matchWords.null = { color: "#808080" }; } if (answers.features.includes("brackets")) { - // Initialize if not exists + theme.schema.matchStartsWith = theme.schema.matchStartsWith || {}; theme.schema.matchEndsWith = theme.schema.matchEndsWith || {}; theme.schema.matchStartsWith["["] = { color: "#ffff00" }; diff --git a/src/cli/theme/transporter.ts b/src/cli/theme/transporter.ts index d3faa9c..7d11ea2 100644 --- a/src/cli/theme/transporter.ts +++ b/src/cli/theme/transporter.ts @@ -100,8 +100,8 @@ export function importThemeFromFile(filePath: string): Theme { const fileContent = fs.readFileSync(filePath, "utf8"); if (filePath.endsWith(".ts") || filePath.endsWith(".js")) { - // Try to extract JSON object from TypeScript/JavaScript file - // Look for patterns like: export const theme = {...} or export default {...} + + const patterns = [ /export\s+const\s+\w+\s*:\s*\w+\s*=\s*(\{[\s\S]*?\})\s*;?\s*$/m, /export\s+default\s+(\{[\s\S]*?\})\s*;?\s*$/m, @@ -112,10 +112,10 @@ export function importThemeFromFile(filePath: string): Theme { const match = fileContent.match(pattern); if (match) { try { - // Clean up the JSON string + const jsonStr = match[1] - .replace(/^\s+/gm, "") // Remove leading whitespace - .replace(/\s+$/gm, "") // Remove trailing whitespace + .replace(/^\s+/gm, "") + .replace(/\s+$/gm, "") .trim(); return JSON.parse(jsonStr); } catch { @@ -159,7 +159,7 @@ export async function importTheme(filename?: string): Promise { const fileContent = fs.readFileSync(themeFile, "utf8"); const themeData = JSON.parse(fileContent); - // Validate the theme structure + const validatedTheme = themePresetSchema.parse(themeData); ui.showInfo(`Importing theme: ${chalk.cyan(validatedTheme.name)}`); @@ -167,7 +167,7 @@ export async function importTheme(filename?: string): Promise { console.log(`Description: ${validatedTheme.description}`); } - // Check if theme already exists + const existingTheme = getTheme(validatedTheme.name); if (existingTheme) { const shouldOverwrite = await confirm({ @@ -185,7 +185,7 @@ export async function importTheme(filename?: string): Promise { } } - // Preview the theme + const showPreview = await confirm({ message: "Preview theme before importing?", default: true, @@ -244,7 +244,7 @@ async function previewImportedTheme(theme: Theme) { "POST /api/login 401 Unauthorized 23ms", ]; - // Temporarily register the theme for preview + const { LogsDX } = await import("../../index"); registerTheme(theme); const logsDX = LogsDX.getInstance({ diff --git a/src/cli/types.ts b/src/cli/types.ts index f9f6170..a700728 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -13,7 +13,7 @@ export const cliOptionsSchema = z.object({ noSpinner: z.boolean().optional().default(false), format: z.enum(["ansi", "html"]).optional(), - // Theme generation options + generateTheme: z.boolean().optional().default(false), listPalettes: z.boolean().optional().default(false), listPatterns: z.boolean().optional().default(false), @@ -25,7 +25,7 @@ export const cliOptionsSchema = z.object({ export type CliOptions = z.infer; export type CommanderOptions = CliOptions; -// UI Interfaces + export interface SpinnerLike { start(): this; succeed(message?: string): this; diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 0d8fba7..5f65d50 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -1,10 +1,10 @@ import { SIZE_UNITS, SIZE_UNIT_MULTIPLIER } from "./constants"; -/** - * Format file size in human-readable format - * @param bytes - Size in bytes - * @returns Formatted size string - */ + + + + + export function formatFileSize(bytes: number): string { if (bytes === 0) { return "0 B"; @@ -20,20 +20,20 @@ export function formatFileSize(bytes: number): string { return `${size.toFixed(1)} ${unit}`; } -/** - * Format number with thousands separators - * @param num - Number to format - * @returns Formatted number string - */ + + + + + export function formatNumber(num: number): string { return num.toLocaleString(); } -/** - * Check if file exists - * @param path - File path - * @returns True if file exists - */ + + + + + export function fileExists(path: string): boolean { try { return require("fs").existsSync(path); @@ -42,10 +42,10 @@ export function fileExists(path: string): boolean { } } -/** - * Ensure directory exists - * @param dirPath - Directory path - */ + + + + export function ensureDir(dirPath: string): void { const fs = require("fs"); if (!fs.existsSync(dirPath)) { diff --git a/src/index.ts b/src/index.ts index b69bc6c..c531806 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,10 +50,10 @@ export class LogsDX { }, }; - /** - * Create a new LogsDX instance - * @param options Configuration options - */ + + + + private constructor(options = {}) { this.options = { theme: "none", @@ -69,11 +69,11 @@ export class LogsDX { this.currentTheme = this.resolveTheme(this.options.theme); } - /** - * Resolve theme from various input formats - * @param theme Theme input in various formats - * @returns Resolved Theme object - */ + + + + + private resolveTheme(theme: string | Theme | ThemePair | undefined): Theme { if (!theme || theme === "none") { return { @@ -147,14 +147,14 @@ export class LogsDX { } } } else { - // Direct Theme object + try { return validateTheme(theme as Theme); } catch (error) { if (this.options.debug) { console.warn("Invalid custom theme:", error); } - // Return no-op theme on validation failure + return { name: "none", description: "No styling applied", @@ -172,22 +172,22 @@ export class LogsDX { } } - /** - * Get a singleton instance of LogsDX - * @param options Configuration options - * @returns LogsDX instance - */ + + + + + static getInstance(options: LogsDXOptions = {}): LogsDX { if (!LogsDX.instance) { LogsDX.instance = new LogsDX(options); } else if (Object.keys(options).length > 0) { - // Update existing instance with new options + LogsDX.instance.options = { ...LogsDX.instance.options, ...options, }; - // If theme changed, update the current theme + if (options.theme) { LogsDX.instance.currentTheme = LogsDX.instance.resolveTheme( options.theme, @@ -201,11 +201,11 @@ export class LogsDX { LogsDX.instance = null; } - /** - * Process a log line with the current theme - * @param line Log line to process - * @returns The styled log line - */ + + + + + processLine(line: string): string { const renderOptions: RenderOptions = { theme: this.currentTheme, @@ -214,13 +214,13 @@ export class LogsDX { escapeHtml: this.options.escapeHtml, }; - // First tokenize the line + const tokens = tokenize(line, this.currentTheme); - // Then apply the theme to get styled tokens + const styledTokens = applyTheme(tokens, this.currentTheme); - // Now render the styled tokens based on output format + if (renderOptions.outputFormat === "html") { if (renderOptions.htmlStyleFormat === "className") { return tokensToClassNames(styledTokens); @@ -228,45 +228,45 @@ export class LogsDX { return tokensToHtml(styledTokens); } } else { - // Default to ANSI output + return tokensToString(styledTokens); } } - /** - * Process multiple log lines - * @param lines Array of log lines - * @returns Array of styled log lines - */ + + + + + processLines(lines: string[]): string[] { return lines.map((line) => this.processLine(line)); } - /** - * Process a log string with multiple lines - * @param logContent Multi-line log content - * @returns The styled log content - */ + + + + + processLog(logContent: string): string { const lines = logContent.split("\n"); const processedLines = this.processLines(lines); return processedLines.join("\n"); } - /** - * Tokenize a log line without applying styling - * @param line Log line to tokenize - * @returns Tokenized log line - */ + + + + + tokenizeLine(line: string): TokenList { return tokenize(line, this.currentTheme); } - /** - * Set the current theme - * @param theme Theme name, custom theme configuration, or theme pair - * @returns True if theme was valid and applied, false otherwise - */ + + + + + setTheme(theme: string | Theme | ThemePair): boolean { try { this.options.theme = theme; @@ -280,77 +280,77 @@ export class LogsDX { } } - /** - * Get the current theme - * @returns Current theme configuration - */ + + + + getCurrentTheme(): Theme { return this.currentTheme; } - /** - * Get all available themes - * @returns Object containing all available themes - */ + + + + getAllThemes(): Record { return getAllThemes(); } - /** - * Get names of all available themes - * @returns Array of theme names - */ + + + + getThemeNames(): string[] { return getThemeNames(); } - /** - * Set the output format - * @param format The output format to use ('ansi' or 'html') - */ + + + + setOutputFormat(format: "ansi" | "html"): void { this.options.outputFormat = format; } - /** - * Set the HTML style format - * @param format The HTML style format to use ('css' or 'className') - */ + + + + setHtmlStyleFormat(format: "css" | "className"): void { this.options.htmlStyleFormat = format; } - /** - * Get the current output format - * @returns The current output format - */ + + + + getCurrentOutputFormat(): "ansi" | "html" { return this.options.outputFormat; } - /** - * Get the current HTML style format - * @returns The current HTML style format - */ + + + + getCurrentHtmlStyleFormat(): "css" | "className" { return this.options.htmlStyleFormat; } } -/** - * Get or create a LogsDX instance - * @param options - Configuration options for LogsDX - * @param options.theme - Theme name, Theme object, or ThemePair for light/dark modes - * @param options.outputFormat - Output format ('ansi' for terminal, 'html' for web) - * @param options.htmlStyleFormat - HTML style format ('css' for inline styles, 'className' for CSS classes) - * @param options.debug - Enable debug mode for additional logging - * @param options.customRules - Custom parsing rules - * @param options.autoAdjustTerminal - Automatically adjust theme based on terminal background - * @returns LogsDX instance - * @example - * const logsdx = getLogsDX({ theme: 'github-dark' }); - * const styledLine = logsdx.processLine('ERROR: Failed to connect'); - */ + + + + + + + + + + + + + + export function getLogsDX(options?: LogsDXOptions): LogsDX { return LogsDX.getInstance(options); } diff --git a/src/renderer/constants.ts b/src/renderer/constants.ts index 0d64e79..9ff70ce 100644 --- a/src/renderer/constants.ts +++ b/src/renderer/constants.ts @@ -41,10 +41,10 @@ export const CLASS_ITALIC = "logsdx-italic"; export const CLASS_UNDERLINE = "logsdx-underline"; export const CLASS_DIM = "logsdx-dim"; -/** - * Check if the terminal supports colors - * @returns True if colors are supported - */ + + + + export function supportsColors(): boolean { if (process.env.NO_COLOR) { return false; @@ -181,12 +181,12 @@ export const TEXT_COLORS: Record = { }, }; -/** - * Get color definition with theme-aware fallback - * @param colorName - The color name from the theme - * @param theme - Optional theme for theme-specific colors - * @returns ColorDefinition or undefined if not found - */ + + + + + + export function getColorDefinition( colorName: string, theme?: Theme, @@ -238,11 +238,11 @@ export function getColorDefinition( return undefined; } -/** - * Convert hex color to RGB values - * @param hex - Hex color string (e.g., "#ff0000") - * @returns RGB array [r, g, b] - */ + + + + + function hexToRgb(hex: string): [number, number, number] { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result diff --git a/src/renderer/detectBackground.ts b/src/renderer/detectBackground.ts index 19ddfc3..e4f59e9 100644 --- a/src/renderer/detectBackground.ts +++ b/src/renderer/detectBackground.ts @@ -88,10 +88,10 @@ function detectFromVSCode(): BackgroundInfo | undefined { }); } -/** - * Detect terminal background using various methods - * @returns Background information for terminal - */ + + + + export function detectTerminalBackground(): BackgroundInfo { const fromColorFgBg = detectFromColorFgBg(); if (fromColorFgBg) { @@ -120,10 +120,10 @@ function matchesColorScheme(scheme: "dark" | "light"): boolean { return query.matches; } -/** - * Detect browser background using media queries - * @returns Background information for browser - */ + + + + export function detectBrowserBackground(): BackgroundInfo { if (!hasMatchMedia()) { return DEFAULT_AUTO_BACKGROUND; @@ -188,10 +188,10 @@ function detectFromLinux(): BackgroundInfo | undefined { }); } -/** - * Detect system background based on platform - * @returns Background information for system - */ + + + + export function detectSystemBackground(): BackgroundInfo { const fromMacOS = detectFromMacOS(); if (fromMacOS) { @@ -221,10 +221,10 @@ function hasHigherConfidence(a: ConfidenceLevel, b: ConfidenceLevel): boolean { return confidenceOrder[a] >= confidenceOrder[b]; } -/** - * Detect background from all available sources - * @returns Background information - */ + + + + export function detectBackground(): BackgroundInfo { if (isBrowser()) { const browserInfo = detectBrowserBackground(); @@ -251,10 +251,10 @@ export function detectBackground(): BackgroundInfo { : systemInfo; } -/** - * Check if background is dark - * @returns True if background is dark - */ + + + + export function isDarkBackground(): boolean { const info = detectBackground(); const isDefaultAuto = info.scheme === "auto" && info.source === "default"; @@ -262,19 +262,19 @@ export function isDarkBackground(): boolean { return info.scheme === "dark" || isDefaultAuto; } -/** - * Check if background is light - * @returns True if background is light - */ + + + + export function isLightBackground(): boolean { const info = detectBackground(); return info.scheme === "light"; } -/** - * Get recommended theme mode based on background - * @returns Recommended theme mode - */ + + + + export function getRecommendedThemeMode(): "light" | "dark" { return isDarkBackground() ? "dark" : "light"; } @@ -316,11 +316,11 @@ function setupMediaQueryListeners( return () => {}; } -/** - * Watch for background changes - * @param callback - Callback function to be called when background changes - * @returns Cleanup function to stop watching - */ + + + + + export function watchBackgroundChanges( callback: (info: BackgroundInfo) => void, ): () => void { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index a6b0428..4e082b0 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -109,12 +109,12 @@ export function tokenToString(token: Token, colorSupport: boolean): string { return result; } -/** - * Convert tokens to a styled string - * @param tokens - The tokens to convert - * @param forceColors - Force color output regardless of terminal detection - * @returns A string with ANSI escape codes for styling - */ + + + + + + export function tokensToString( tokens: TokenList, forceColors?: boolean, @@ -229,12 +229,12 @@ export function tokenToHtml(token: Token, options: RenderOptions): string { return wrapInSpan(content, css); } -/** - * Convert tokens to HTML with inline CSS styles - * @param tokens - The tokens to convert - * @param options - Render options - * @returns HTML string with inline CSS styles - */ + + + + + + export function tokensToHtml( tokens: TokenList, options: RenderOptions = {}, @@ -306,12 +306,12 @@ export function tokenToClassName(token: Token, options: RenderOptions): string { return wrapInSpanWithClass(content, classes); } -/** - * Convert tokens to HTML with CSS class names - * @param tokens - The tokens to convert - * @param options - Render options - * @returns HTML with CSS class names for styling - */ + + + + + + export function tokensToClassNames( tokens: TokenList, options: RenderOptions = {}, @@ -321,13 +321,13 @@ export function tokensToClassNames( .join(EMPTY_STRING); } -/** - * Render a line with the specified options - * @param line - The line to render - * @param theme - The theme to use - * @param options - Rendering options - * @returns The rendered line - */ + + + + + + + export function renderLine( line: string, theme?: Theme, @@ -346,21 +346,21 @@ export function renderLine( return tokensToString(styledTokens); } -/** - * Highlight a line (for line highlighting) - * @param line - The line to highlight - * @returns The highlighted line - */ + + + + + export function highlightLine(line: string): string { return `${LINE_HIGHLIGHT_BG}${line}${RESET}`; } -/** - * Apply color to text using ANSI escape codes - * @param text - The text to color - * @param color - The color to apply - * @returns The colored text - */ + + + + + + export function applyColor(text: string, color: string): string { const colorDef = getColorDefinition(color); if (!colorDef) { @@ -369,48 +369,48 @@ export function applyColor(text: string, color: string): string { return `${colorDef.ansi}${text}${STYLE_CODES.resetColor}`; } -/** - * Apply bold style to text using ANSI escape codes - * @param text - The text to style - * @returns The styled text - */ + + + + + export function applyBold(text: string): string { return `${STYLE_CODES.bold}${text}${STYLE_CODES.resetBold}`; } -/** - * Apply italic style to text using ANSI escape codes - * @param text - The text to style - * @returns The styled text - */ + + + + + export function applyItalic(text: string): string { return `${STYLE_CODES.italic}${text}${STYLE_CODES.resetItalic}`; } -/** - * Apply underline style to text using ANSI escape codes - * @param text - The text to style - * @returns The styled text - */ + + + + + export function applyUnderline(text: string): string { return `${STYLE_CODES.underline}${text}${STYLE_CODES.resetUnderline}`; } -/** - * Apply dim style to text using ANSI escape codes - * @param text - The text to style - * @returns The styled text - */ + + + + + export function applyDim(text: string): string { return `${STYLE_CODES.dim}${text}${STYLE_CODES.resetDim}`; } -/** - * Apply background color to text using ANSI escape codes - * @param text - The text to color - * @param color - The color to apply - * @returns The colored text - */ + + + + + + export function applyBackgroundColor(text: string, color: string): string { const colorDef = BACKGROUND_COLORS[color]; if (!colorDef) { @@ -419,33 +419,33 @@ export function applyBackgroundColor(text: string, color: string): string { return `${colorDef.ansi}${text}${STYLE_CODES.resetBackground}`; } -/** - * Creates a 256-color foreground color code - * @param code Color code (0-255) - * @returns ANSI color code string - */ + + + + + export function fg256(code: number): string { return `\x1b[38;5;${code}m`; } -/** - * Creates a 24-bit RGB foreground color code - * @param r Red (0-255) - * @param g Green (0-255) - * @param b Blue (0-255) - * @returns ANSI color code string - */ + + + + + + + export function fgRGB(r: number, g: number, b: number): string { return `\x1b[38;2;${r};${g};${b}m`; } -/** - * Render multiple lines with the specified options - * @param lines - The lines to render - * @param theme - The theme to use - * @param options - Rendering options - * @returns The rendered lines - */ + + + + + + + export function renderLines( lines: ReadonlyArray, theme?: Theme, diff --git a/src/renderer/lightBox.ts b/src/renderer/lightBox.ts index 49e7fe3..cae8bff 100644 --- a/src/renderer/lightBox.ts +++ b/src/renderer/lightBox.ts @@ -52,11 +52,11 @@ const DEFAULT_PADDING = 2; const DEFAULT_BORDER = true; const DEFAULT_BORDER_STYLE: BorderStyle = "rounded"; -/** - * Get the background color for a theme - * @param theme - Theme object or theme name string - * @returns ANSI background color code - */ + + + + + export function getThemeBackground(theme: Theme | string): string { const themeName = typeof theme === "string" ? theme : theme.name; return THEME_BACKGROUNDS[themeName] || DEFAULT_BACKGROUND; @@ -132,13 +132,13 @@ function createPaddedLine( return `${backgroundColor}${content}${RESET}`; } -/** - * Render a line in a light box - * @param line - The line to render - * @param theme - Theme object or theme name string - * @param options - Light box rendering options - * @returns Rendered line string - */ + + + + + + + export function renderLightBoxLine( line: string, theme: Theme | string, @@ -163,14 +163,14 @@ export function renderLightBoxLine( ); } -/** - * Render a complete light box with multiple lines - * @param lines - Array of lines to render - * @param theme - Theme object or theme name string - * @param title - Optional title for the box - * @param options - Light box rendering options - * @returns Array of rendered line strings - */ + + + + + + + + export function renderLightBox( lines: ReadonlyArray, theme: Theme | string, @@ -199,11 +199,11 @@ export function renderLightBox( return result; } -/** - * Check if a theme is a light theme - * @param theme - Theme object or theme name string - * @returns True if the theme is a light theme - */ + + + + + export function isLightTheme(theme: Theme | string): boolean { if (typeof theme === "object" && theme.mode) { return theme.mode === "light"; @@ -215,10 +215,10 @@ export function isLightTheme(theme: Theme | string): boolean { return lowerName.includes("light") || lowerName.includes("white"); } -/** - * Check if terminal has dark background - * @returns True if terminal background is dark - */ + + + + export function isTerminalDark(): boolean { return isDarkBackground(); } diff --git a/src/schema/validator.ts b/src/schema/validator.ts index 739add2..7357a1c 100644 --- a/src/schema/validator.ts +++ b/src/schema/validator.ts @@ -91,11 +91,11 @@ export function createThemeValidationError(error: unknown): Error { return createValidationError(message, error); } -/** - * Validates a theme configuration with enhanced error messages - * @param theme The theme configuration to validate - * @returns The validated theme configuration or throws an error - */ + + + + + export function validateTheme(theme: unknown): Theme { try { return parseTheme(theme); @@ -104,11 +104,11 @@ export function validateTheme(theme: unknown): Theme { } } -/** - * Safely validates a theme configuration - * @param theme The theme configuration to validate - * @returns An object with success flag and either the validated data or error - */ + + + + + export function validateThemeSafe(theme: unknown): { success: boolean; data?: Theme; diff --git a/src/themes/color-constants.ts b/src/themes/color-constants.ts index 2329543..25922ff 100644 --- a/src/themes/color-constants.ts +++ b/src/themes/color-constants.ts @@ -1,6 +1,6 @@ -/** - * Color palettes for WCAG theme utilities - */ + + + export const colors = { gray: { 50: "#f9fafb", 100: "#f3f4f6", 800: "#1f2937", 900: "#111827" }, diff --git a/src/themes/index.ts b/src/themes/index.ts index ae0d04a..8877007 100644 --- a/src/themes/index.ts +++ b/src/themes/index.ts @@ -11,35 +11,35 @@ import { } from "./builder"; import type { ColorPalette, SimpleThemeConfig } from "./builder"; -/** - * Get a theme by name - * @param themeName Name of the theme to get - * @returns The requested theme or default theme if not found - */ + + + + + export function getTheme(themeName: string): Theme { return THEMES[themeName] || (THEMES[DEFAULT_THEME] as Theme); } -/** - * Get all available themes - * @returns Object containing all available themes - */ + + + + export function getAllThemes(): Record { return THEMES; } -/** - * Get names of all available themes - * @returns Array of theme names - */ + + + + export function getThemeNames(): string[] { return Object.keys(THEMES); } -/** - * Register a custom theme to make it available for use - * @param theme The theme to register - */ + + + + export function registerTheme(theme: Theme): void { THEMES[theme.name] = theme; } diff --git a/src/themes/template.ts b/src/themes/template.ts index e062295..57dca40 100644 --- a/src/themes/template.ts +++ b/src/themes/template.ts @@ -129,7 +129,7 @@ export const themeGeneratorConfigSchema = z.object({ export type ThemeGeneratorConfig = z.infer; -// Built-in color palettes with accessibility considerations + export const COLOR_PALETTES: ColorPalette[] = [ { name: "github-light", @@ -254,7 +254,7 @@ export const COLOR_PALETTES: ColorPalette[] = [ }, ]; -// Common pattern presets for different log types + export const PATTERN_PRESETS: PatternPreset[] = [ { name: "http-api", diff --git a/src/themes/utils.ts b/src/themes/utils.ts index e7b35c0..7d7a9fc 100644 --- a/src/themes/utils.ts +++ b/src/themes/utils.ts @@ -1,31 +1,31 @@ -/** - * Theme utility functions for color accessibility and contrast - */ + + + import { colors } from "./color-constants"; -/** - * Determine if a hex color is dark or light based on luminance - */ + + + export function isDarkColor(hex: string): boolean { - // Remove # if present + const color = hex.replace("#", ""); - // Convert to RGB + const r = parseInt(color.slice(0, 2), 16); const g = parseInt(color.slice(2, 4), 16); const b = parseInt(color.slice(4, 6), 16); - // Calculate relative luminance + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance < 0.5; } -/** - * Get accessible text colors for a given background - * Returns Tailwind colors that meet WCAG contrast requirements - */ + + + + export function getAccessibleTextColors( backgroundColor: string, contrastLevel: "AAA" | "AA" = "AA", @@ -33,7 +33,7 @@ export function getAccessibleTextColors( const isDark = isDarkColor(backgroundColor); if (isDark) { - // Dark background - use light colors + return { text: contrastLevel === "AAA" ? colors.gray[50] : colors.gray[100], info: contrastLevel === "AAA" ? colors.sky[300] : colors.sky[400], @@ -45,7 +45,7 @@ export function getAccessibleTextColors( string: contrastLevel === "AAA" ? colors.lime[300] : colors.lime[400], }; } else { - // Light background - use dark colors + return { text: contrastLevel === "AAA" ? colors.gray[900] : colors.gray[800], info: contrastLevel === "AAA" ? colors.sky[700] : colors.sky[600], @@ -59,9 +59,9 @@ export function getAccessibleTextColors( } } -/** - * Map WCAG contrast ratio to compliance level - */ + + + export function getWCAGLevel( ratio: number, isLargeText: boolean = false, @@ -73,14 +73,14 @@ export function getWCAGLevel( } else { if (ratio >= 7) return "AAA"; if (ratio >= 4.5) return "AA"; - if (ratio >= 3) return "A"; // Only for large text + if (ratio >= 3) return "A"; return "FAIL"; } } -/** - * Get WCAG recommendations based on contrast ratio - */ + + + export function getWCAGRecommendations(ratio: number): string[] { const recommendations: string[] = []; diff --git a/src/tokenizer/index.ts b/src/tokenizer/index.ts index 92cad6e..aff8811 100644 --- a/src/tokenizer/index.ts +++ b/src/tokenizer/index.ts @@ -354,11 +354,11 @@ export function addThemeRules(lexer: SimpleLexer, theme: Theme): void { } } -/** - * Create a lexer with theme-specific rules - * @param theme - The theme to use for tokenization - * @returns A lexer with theme-specific rules - */ + + + + + export function createLexer(theme?: Theme): SimpleLexer { const lexer = new SimpleLexer(); @@ -465,12 +465,12 @@ export function convertLexerTokens( return lexerTokens.map(convertLexerToken); } -/** - * Tokenize a log line using theme-specific patterns - * @param line - The log line to tokenize - * @param theme - Optional theme to use for tokenization - * @returns A list of tokens - */ + + + + + + export function tokenize(line: string, theme?: Theme): TokenList { try { if (shouldUseDefaultToken(theme)) { @@ -643,12 +643,12 @@ export function applyTokenStyle(token: Token, theme: Theme): Token { return applyDefaultStyle(token, theme); } -/** - * Apply theme styling to tokens - * @param tokens - The tokens to style - * @param theme - The theme to apply - * @returns The styled tokens - */ + + + + + + export function applyTheme(tokens: TokenList, theme: Theme): TokenList { if (!theme || !theme.schema) { return tokens; diff --git a/src/types.ts b/src/types.ts index cda4204..706fd4d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ -/** - * Style codes that can be applied to text - */ + + + export type StyleCode = | "bold" | "italic" @@ -10,21 +10,21 @@ export type StyleCode = | "reverse" | "strikethrough"; -/** - * Options for styling text in logs - */ + + + export interface StyleOptions { - /** The color to apply (hex, rgb, or color name) */ + color: string; - /** Optional style codes to apply */ + styleCodes?: StyleCode[]; - /** Format for HTML output */ + htmlStyleFormat?: "css" | "className"; } -/** - * Filter and validate style codes - */ + + + export function filterStyleCodes(codes: string[] | undefined): StyleCode[] { if (!codes) return []; const validCodes: StyleCode[] = [ @@ -81,14 +81,14 @@ export interface Theme { export type ThemePreset = Theme; -/** - * Theme pair for automatic light/dark mode switching - * Allows specifying different themes for light and dark environments - */ + + + + export interface ThemePair { - /** Theme to use in light mode (theme name or Theme object) */ + light: string | Theme; - /** Theme to use in dark mode (theme name or Theme object) */ + dark: string | Theme; } @@ -96,10 +96,10 @@ export interface LogsDXOptions { theme?: string | Theme | ThemePair; outputFormat?: "ansi" | "html"; htmlStyleFormat?: "css" | "className"; - escapeHtml?: boolean; // Whether to escape HTML in output (default: true) + escapeHtml?: boolean; debug?: boolean; customRules?: Record; - autoAdjustTerminal?: boolean; // Automatically adjust themes for terminal visibility + autoAdjustTerminal?: boolean; } export type LogLevel = "debug" | "info" | "warn" | "error"; diff --git a/src/utils/ascii.ts b/src/utils/ascii.ts index 728b42b..aa09fe7 100644 --- a/src/utils/ascii.ts +++ b/src/utils/ascii.ts @@ -12,12 +12,12 @@ export function textSync( verticalLayout?: string; }, ): string { - // For LogsDX, return the predefined ASCII art + if (text === "LogsDX") { return LOGSDX_ASCII; } - // For other text, return as-is + return text; } diff --git a/src/utils/boxen.ts b/src/utils/boxen.ts index ce88ce2..d359fc8 100644 --- a/src/utils/boxen.ts +++ b/src/utils/boxen.ts @@ -84,14 +84,14 @@ export function boxen(text: string, options: BoxenOptions = {}): string { const result: string[] = []; - // Top margin + for (let i = 0; i < margin.top; i++) { result.push(""); } const leftMargin = " ".repeat(margin.left); - // Top border + const topBorder = options.title ? border.topLeft + ` ${options.title} ` + @@ -102,14 +102,14 @@ export function boxen(text: string, options: BoxenOptions = {}): string { : border.topLeft + border.horizontal.repeat(boxWidth) + border.topRight; result.push(leftMargin + topBorder); - // Top padding + for (let i = 0; i < padding.top; i++) { result.push( leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical, ); } - // Content + lines.forEach((line) => { const cleanLength = line.replace(/\x1B\[[0-9;]*m/g, "").length; const paddingRight = " ".repeat(Math.max(0, contentWidth - cleanLength)); @@ -124,14 +124,14 @@ export function boxen(text: string, options: BoxenOptions = {}): string { ); }); - // Bottom padding + for (let i = 0; i < padding.bottom; i++) { result.push( leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical, ); } - // Bottom border + result.push( leftMargin + border.bottomLeft + @@ -139,7 +139,7 @@ export function boxen(text: string, options: BoxenOptions = {}): string { border.bottomRight, ); - // Bottom margin + for (let i = 0; i < margin.bottom; i++) { result.push(""); } diff --git a/src/utils/colors.ts b/src/utils/colors.ts index 485a16a..a87092f 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -1,5 +1,5 @@ const styles = { - // Colors + black: "\x1B[30m", red: "\x1B[31m", green: "\x1B[32m", @@ -10,7 +10,7 @@ const styles = { white: "\x1B[37m", gray: "\x1B[90m", - // Bright colors + redBright: "\x1B[91m", greenBright: "\x1B[92m", yellowBright: "\x1B[93m", @@ -19,13 +19,13 @@ const styles = { cyanBright: "\x1B[96m", whiteBright: "\x1B[97m", - // Modifiers + bold: "\x1B[1m", dim: "\x1B[2m", italic: "\x1B[3m", underline: "\x1B[4m", - // Reset + reset: "\x1B[0m", }; @@ -41,7 +41,7 @@ function createChainableColor(appliedStyles: string[] = []): any { return `${prefix}${text}${styles.reset}`; }; - // Add all style properties + Object.keys(styles).forEach((key) => { if (key === "reset") return; Object.defineProperty(fn, key, { @@ -59,7 +59,7 @@ function createChainableColor(appliedStyles: string[] = []): any { export const colors = createChainableColor(); -// Export individual functions for direct use + export const red = createColorFunction(styles.red); export const green = createColorFunction(styles.green); export const yellow = createColorFunction(styles.yellow); diff --git a/src/utils/gradient.ts b/src/utils/gradient.ts index 74d2afc..7cd1c8f 100644 --- a/src/utils/gradient.ts +++ b/src/utils/gradient.ts @@ -2,7 +2,7 @@ export function gradient(colors: string[]): { (text: string): string; multiline(text: string): string; } { - // Use cyan as default gradient color (simple implementation) + const applyGradient = (text: string) => `\x1B[36m${text}\x1B[0m`; applyGradient.multiline = (text: string) => { diff --git a/src/utils/spinner.ts b/src/utils/spinner.ts index bbbf0a8..bca4f6b 100644 --- a/src/utils/spinner.ts +++ b/src/utils/spinner.ts @@ -1,6 +1,6 @@ -/** - * Lightweight spinner utility to replace ora - */ + + + export interface Spinner { start(): Spinner; @@ -30,7 +30,7 @@ export function spinner(initialText: string): Spinner { if (isSpinning) return instance; isSpinning = true; - process.stdout.write("\x1B[?25l"); // Hide cursor + process.stdout.write("\x1B[?25l"); interval = setInterval(() => { const frame = frames[frameIndex]; @@ -61,8 +61,8 @@ export function spinner(initialText: string): Spinner { interval = null; } if (isSpinning) { - process.stdout.write("\r\x1B[K"); // Clear line - process.stdout.write("\x1B[?25h"); // Show cursor + process.stdout.write("\r\x1B[K"); + process.stdout.write("\x1B[?25h"); isSpinning = false; } return instance; diff --git a/tests/unit/cli/index.test.ts b/tests/unit/cli/index.test.ts index 5b0404b..e7338be 100644 --- a/tests/unit/cli/index.test.ts +++ b/tests/unit/cli/index.test.ts @@ -5,7 +5,7 @@ import fs from "fs"; import os from "os"; import path from "path"; -// Using the imported CliOptions type from ./types + describe("parseArgs", () => { test("should parse basic theme argument", () => { diff --git a/tests/unit/cli/theme/generator.test.ts b/tests/unit/cli/theme/generator.test.ts index ec85f75..366cc4f 100644 --- a/tests/unit/cli/theme/generator.test.ts +++ b/tests/unit/cli/theme/generator.test.ts @@ -22,13 +22,13 @@ describe("Theme Generator", () => { it("should validate rgb colors", () => { expect(validateColorInput("rgb(255, 0, 0)")).toBe(true); expect(validateColorInput("rgba(255, 0, 0, 0.5)")).toBe(true); - expect(validateColorInput("rgb(256, 0, 0)")).toBe(true); // Still valid format + expect(validateColorInput("rgb(256, 0, 0)")).toBe(true); }); it("should validate named colors", () => { expect(validateColorInput("red")).toBe(true); expect(validateColorInput("blue")).toBe(true); - expect(validateColorInput("notacolor")).toBe(true); // We accept any string as a named color + expect(validateColorInput("notacolor")).toBe(true); }); }); @@ -120,11 +120,11 @@ describe("Theme Generator", () => { expect(theme.schema.matchWords).toHaveProperty("false"); expect(theme.schema.matchWords).toHaveProperty("null"); - // Check that matchStartsWith and matchEndsWith exist + expect(theme.schema.matchStartsWith).toBeDefined(); expect(theme.schema.matchEndsWith).toBeDefined(); - // Check the bracket properties + expect(theme.schema.matchStartsWith?.["["]).toBeDefined(); expect(theme.schema.matchEndsWith?.["]"]).toBeDefined(); }); diff --git a/tests/unit/cli/theme/transporter.test.ts b/tests/unit/cli/theme/transporter.test.ts index 05e8d25..d3ee698 100644 --- a/tests/unit/cli/theme/transporter.test.ts +++ b/tests/unit/cli/theme/transporter.test.ts @@ -8,19 +8,19 @@ import { } from "../../../../src/cli/theme/transporter"; import type { Theme } from "../../../../src/types"; -// Create a temporary test directory + const TEST_DIR = join(process.cwd(), ".test-themes"); describe("Theme Transporter", () => { beforeEach(() => { - // Create test directory + if (!existsSync(TEST_DIR)) { mkdirSync(TEST_DIR, { recursive: true }); } }); afterEach(() => { - // Clean up test directory + if (existsSync(TEST_DIR)) { rmSync(TEST_DIR, { recursive: true, force: true }); } @@ -121,7 +121,7 @@ describe("Theme Transporter", () => { it("should validate imported theme", () => { const filePath = join(TEST_DIR, "invalid-theme.json"); const invalidTheme = { - // Missing required fields + description: "Invalid theme", }; writeFileSync(filePath, JSON.stringify(invalidTheme, null, 2)); @@ -144,7 +144,7 @@ describe("Theme Transporter", () => { describe("listThemeFiles", () => { it("should list theme files in directory", () => { - // Create some theme files + writeFileSync(join(TEST_DIR, "theme1.json"), JSON.stringify(sampleTheme)); writeFileSync(join(TEST_DIR, "theme2.json"), JSON.stringify(sampleTheme)); writeFileSync( @@ -195,26 +195,26 @@ describe("Theme Transporter", () => { it("should round-trip theme through export and import", () => { const filePath = join(TEST_DIR, "round-trip.json"); - // Export + exportThemeToFile(sampleTheme, filePath, "json"); - // Import + const imported = importThemeFromFile(filePath); - // Verify + expect(imported).toEqual(sampleTheme); }); it("should handle TypeScript round-trip", () => { const filePath = join(TEST_DIR, "round-trip.ts"); - // Export as TypeScript + exportThemeToFile(sampleTheme, filePath, "typescript"); - // Import + const imported = importThemeFromFile(filePath); - // Verify core properties (TS export adds formatting) + expect(imported.name).toBe(sampleTheme.name); expect(imported.description).toBe(sampleTheme.description); expect(imported.mode).toBe(sampleTheme.mode); diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 8e6d5a6..c0df4a8 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -3,11 +3,11 @@ import stripAnsi from "strip-ansi"; import { LogsDX, getLogsDX } from "../../src/index"; describe("LogsDX", () => { - // Save original console methods + const originalConsoleWarn = console.warn; let consoleWarnings: string[] = []; - // Setup and teardown + beforeEach(() => { LogsDX.resetInstance(); consoleWarnings = []; @@ -33,9 +33,9 @@ describe("LogsDX", () => { }); test("applies custom options when provided", () => { - // First reset to ensure we're starting fresh + LogsDX.resetInstance(); - // Then create with custom options + const customInstance = LogsDX.getInstance({ theme: "oh-my-zsh" }); expect(customInstance.getCurrentTheme().name).toBe("oh-my-zsh"); }); @@ -64,7 +64,7 @@ describe("LogsDX", () => { const line = "test line"; const result = instance.processLine(line); - // Just verify it returns a string + expect(typeof result).toBe("string"); }); @@ -108,7 +108,7 @@ describe("LogsDX", () => { expect(tokens).toBeInstanceOf(Array); expect(tokens.length).toBeGreaterThan(0); - // The entire string should be tokenized + const totalContent = tokens.map((t) => t.content).join(""); expect(totalContent).toBe("test line"); }); @@ -117,7 +117,7 @@ describe("LogsDX", () => { describe("setTheme", () => { test("sets theme by name", () => { const instance = LogsDX.getInstance(); - // Use a theme we know exists + const result = instance.setTheme("oh-my-zsh"); expect(result).toBe(true); expect(instance.getCurrentTheme().name).toBe("oh-my-zsh"); @@ -138,9 +138,9 @@ describe("LogsDX", () => { test("returns true even for invalid theme (fails silently)", () => { const instance = LogsDX.getInstance({ debug: true }); - // @ts-expect-error - Testing invalid theme object + expect(instance.setTheme({ invalid: "theme" })).toBe(true); - // Should default to 'none' theme when invalid + expect(instance.getCurrentTheme().name).toBe("none"); }); }); @@ -199,57 +199,57 @@ describe("LogsDX", () => { }); describe("ANSI theming integration", () => { - // Save original console methods + const originalConsoleWarn = console.warn; beforeEach(() => { - // Reset the instance before each test to ensure clean state + LogsDX.resetInstance(); - // Silence warnings during these tests + console.warn = () => {}; }); afterEach(() => { - // Restore console.warn + console.warn = originalConsoleWarn; }); test("applies theme colors to ANSI output", () => { - // Create a fresh instance with explicit ANSI output format + const instance = LogsDX.getInstance({ theme: "oh-my-zsh", - outputFormat: "ansi", // Explicitly set output format to ansi + outputFormat: "ansi", }); - // Verify the output format is set correctly + expect(instance.getCurrentOutputFormat()).toBe("ansi"); - // Process lines with different log levels + const errorLine = instance.processLine("ERROR: This is an error message"); const warnLine = instance.processLine("WARN: This is a warning message"); const infoLine = instance.processLine("INFO: This is an info message"); - // Strip ANSI codes and check the plain text content + expect(stripAnsi(errorLine)).toBe("ERROR: This is an error message"); expect(stripAnsi(warnLine)).toBe("WARN: This is a warning message"); expect(stripAnsi(infoLine)).toBe("INFO: This is an info message"); - // Test that different themes produce different output + LogsDX.resetInstance(); const dracula = LogsDX.getInstance({ theme: "dracula", - outputFormat: "ansi", // Explicitly set output format to ansi + outputFormat: "ansi", }); const draculaError = dracula.processLine("ERROR: This is an error message"); - // The original text should be preserved + expect(stripAnsi(draculaError)).toBe("ERROR: This is an error message"); }); - // Helper function to strip ANSI escape codes for testing + test("applies style codes like bold and italic", () => { - // Create a fresh instance with explicit ANSI output format and a custom theme + const instance = LogsDX.getInstance({ theme: { name: "test-theme", @@ -263,14 +263,14 @@ describe("ANSI theming integration", () => { outputFormat: "ansi", }); - // Process a line with a keyword that should have styling + const errorLine = instance.processLine("ERROR: Critical failure"); - // Strip ANSI codes and check the plain text content + const plainText = stripAnsi(errorLine); expect(plainText).toBe("ERROR: Critical failure"); - // Verify the output format is set correctly + expect(instance.getCurrentOutputFormat()).toBe("ansi"); }); }); diff --git a/tests/unit/renderer/constants.test.ts b/tests/unit/renderer/constants.test.ts index 6e50320..51f7e56 100644 --- a/tests/unit/renderer/constants.test.ts +++ b/tests/unit/renderer/constants.test.ts @@ -45,7 +45,7 @@ describe("Color Support Detection", () => { (process.stdout as NodeJS.WriteStream & { isTTY?: boolean }).isTTY = true; delete process.env.TERM; - // In Bun runtime, colors are supported even without TERM + const expectedResult = typeof Bun !== "undefined" ? true : false; expect(supportsColors()).toBe(expectedResult); }); diff --git a/tests/unit/renderer/index.test.ts b/tests/unit/renderer/index.test.ts index 799aba8..e19cea1 100644 --- a/tests/unit/renderer/index.test.ts +++ b/tests/unit/renderer/index.test.ts @@ -17,7 +17,7 @@ describe("Renderer", () => { describe("renderLine", () => { test("renders a simple line with default options", () => { const result = renderLine("test line"); - // We need to check for individual characters since they're styled separately + expect(result).toContain("t"); expect(result).toContain("e"); expect(result).toContain("s"); @@ -29,7 +29,7 @@ describe("Renderer", () => { outputFormat: "html", htmlStyleFormat: "css", }); - // Check for HTML span tags and the content + expect(result).toContain(""); @@ -40,7 +40,7 @@ describe("Renderer", () => { outputFormat: "html", htmlStyleFormat: "className", }); - // Check for HTML span tags with class names + expect(result).toContain(""); @@ -87,9 +87,9 @@ describe("Renderer", () => { }, }, ]; - const result = tokensToString(tokens, true); // Force colors for testing + const result = tokensToString(tokens, true); expect(result).toContain("error"); - expect(result).toContain("\x1b[31m"); // Red color ANSI code + expect(result).toContain("\x1b[31m"); }); test("applies multiple style codes to tokens", () => { @@ -104,10 +104,10 @@ describe("Renderer", () => { }, }, ]; - const result = tokensToString(tokens, true); // Force colors for testing + const result = tokensToString(tokens, true); expect(result).toContain("important"); - expect(result).toContain("\x1b[1m"); // Bold - expect(result).toContain("\x1b[4m"); // Underline + expect(result).toContain("\x1b[1m"); + expect(result).toContain("\x1b[4m"); }); }); @@ -193,7 +193,7 @@ describe("Renderer", () => { test("applyColor adds ANSI color codes", () => { const result = applyColor("text", "red"); expect(result).toContain("text"); - // Since the implementation returns objects, let's check the structure + expect(typeof result).toBe("string"); expect(result).toMatch(/text/); }); @@ -221,7 +221,7 @@ describe("Renderer", () => { test("applyBackgroundColor adds ANSI background color", () => { const result = applyBackgroundColor("text", "blue"); expect(result).toContain("text"); - // Since the implementation returns objects, let's check the structure + expect(typeof result).toBe("string"); expect(result).toMatch(/text/); }); diff --git a/tests/unit/schema/validator.test.ts b/tests/unit/schema/validator.test.ts index 860cdd6..17dfaca 100644 --- a/tests/unit/schema/validator.test.ts +++ b/tests/unit/schema/validator.test.ts @@ -125,7 +125,7 @@ describe("Schema Validator", () => { test("throws on invalid theme", () => { const invalidTheme = { name: "Dark Theme", - // Missing schema property + }; expect(() => validateTheme(invalidTheme)).toThrow(); @@ -153,7 +153,7 @@ describe("Schema Validator", () => { test("returns error for invalid theme", () => { const invalidTheme = { name: "Dark Theme", - // Missing schema property + }; const result = validateThemeSafe(invalidTheme); @@ -167,11 +167,11 @@ describe("Schema Validator", () => { const jsonSchema = convertTokenSchemaToJson(); expect(jsonSchema).toHaveProperty("$schema"); - // The structure might be different than expected, check what's actually returned + expect(typeof jsonSchema).toBe("object"); - // Looking at the implementation, the name is passed as an option to zodToJsonSchema - // but it might be stored differently in the output + + const hasNameReference = JSON.stringify(jsonSchema).includes("Token"); expect(hasNameReference).toBe(true); }); @@ -182,11 +182,11 @@ describe("Schema Validator", () => { const jsonSchema = convertThemeSchemaToJson(); expect(jsonSchema).toHaveProperty("$schema"); - // The structure might be different than expected, check what's actually returned + expect(typeof jsonSchema).toBe("object"); - // Looking at the implementation, the name is passed as an option to zodToJsonSchema - // but it might be stored differently in the output + + const hasNameReference = JSON.stringify(jsonSchema).includes("Theme"); expect(hasNameReference).toBe(true); }); diff --git a/tests/unit/themes/index.test.ts b/tests/unit/themes/index.test.ts index 0acdf84..126e8c1 100644 --- a/tests/unit/themes/index.test.ts +++ b/tests/unit/themes/index.test.ts @@ -8,11 +8,11 @@ import { import { THEMES, DEFAULT_THEME } from "../../../src/themes/constants"; describe("Theme Management", () => { - // Save original console.log + const originalConsoleLog = console.log; let consoleOutput: string[] = []; - // Mock console.log before each test + beforeEach(() => { consoleOutput = []; console.log = (message: string) => { @@ -20,7 +20,7 @@ describe("Theme Management", () => { }; }); - // Restore console.log after each test + afterEach(() => { console.log = originalConsoleLog; }); @@ -72,7 +72,7 @@ describe("Theme Management", () => { registerTheme(testTheme); - // Verify theme was registered + expect(getTheme("test-theme")).toEqual(testTheme); expect(getThemeNames()).toContain("test-theme"); expect(getAllThemes()["test-theme"]).toEqual(testTheme); @@ -106,7 +106,7 @@ describe("Theme Management", () => { registerTheme(firstTheme); registerTheme(secondTheme); - // Should have the second theme + expect(getTheme("overwrite-test")).toEqual(secondTheme); expect(getTheme("overwrite-test").description).toBe("Second version"); }); diff --git a/tests/unit/themes/template.test.ts b/tests/unit/themes/template.test.ts index 51bf1d1..9d9e94a 100644 --- a/tests/unit/themes/template.test.ts +++ b/tests/unit/themes/template.test.ts @@ -239,7 +239,7 @@ describe("Theme Generation", () => { const theme = generateTemplate(config); expect(theme.schema.matchWords?.CUSTOM).toBeDefined(); - expect(theme.schema.matchWords?.CUSTOM.color).toBe("#d1242f"); // github-light error color + expect(theme.schema.matchWords?.CUSTOM.color).toBe("#d1242f"); expect(theme.schema.matchWords?.CUSTOM.styleCodes).toEqual(["bold"]); }); @@ -252,11 +252,11 @@ describe("Theme Generation", () => { const theme = generateTemplate(config); - // Should have words from both presets - expect(theme.schema.matchWords?.INFO).toBeDefined(); // from log-levels - expect(theme.schema.matchWords?.GET).toBeDefined(); // from http-api + + expect(theme.schema.matchWords?.INFO).toBeDefined(); + expect(theme.schema.matchWords?.GET).toBeDefined(); - // Should have patterns from both presets + const timestampPattern = theme.schema.matchPatterns?.find( (p) => p.name === "timestamp-iso", ); diff --git a/tests/unit/themes/utils.test.ts b/tests/unit/themes/utils.test.ts index f08de5c..509f51b 100644 --- a/tests/unit/themes/utils.test.ts +++ b/tests/unit/themes/utils.test.ts @@ -38,8 +38,8 @@ describe("themes/utils", () => { }); test("correctly identifies medium luminance colors", () => { - expect(isDarkColor("#808080")).toBe(false); // Medium gray - expect(isDarkColor("#404040")).toBe(true); // Dark medium gray + expect(isDarkColor("#808080")).toBe(false); + expect(isDarkColor("#404040")).toBe(true); }); }); @@ -102,7 +102,7 @@ describe("themes/utils", () => { const colorsAA = getAccessibleTextColors("#000000", "AA"); const colorsAAA = getAccessibleTextColors("#000000", "AAA"); - // AAA should provide higher contrast (lighter colors for dark background) + expect(colorsAAA.text).not.toBe(colorsAA.text); }); @@ -110,7 +110,7 @@ describe("themes/utils", () => { const colorsAA = getAccessibleTextColors("#FFFFFF", "AA"); const colorsAAA = getAccessibleTextColors("#FFFFFF", "AAA"); - // AAA should provide higher contrast (darker colors for light background) + expect(colorsAAA.text).not.toBe(colorsAA.text); }); }); diff --git a/tests/unit/tokenizer/index.test.ts b/tests/unit/tokenizer/index.test.ts index 46bd241..bafab6d 100644 --- a/tests/unit/tokenizer/index.test.ts +++ b/tests/unit/tokenizer/index.test.ts @@ -36,7 +36,7 @@ describe("Tokenizer", () => { expect(tokens).toBeInstanceOf(Array); expect(tokens.length).toBeGreaterThan(0); - // The entire string should be tokenized + const totalContent = tokens.map((t) => t.content).join(""); expect(totalContent).toBe(line); }); @@ -45,15 +45,15 @@ describe("Tokenizer", () => { const line = "2023-01-01T12:00:00Z ERROR: Something went wrong"; const tokens = tokenize(line); - // The default tokenizer might not identify timestamps and log levels - // Let's just check that the tokens contain the expected content + + const content = tokens.map((t) => t.content).join(""); expect(content).toBe(line); - // Check that we have multiple tokens + expect(tokens.length).toBeGreaterThan(1); - // Check that at least one token contains "ERROR" + const hasError = tokens.some((t) => t.content.includes("ERROR")); expect(hasError).toBe(true); }); @@ -61,12 +61,12 @@ describe("Tokenizer", () => { test("handles whitespace according to theme preferences", () => { const line = "test with spaces"; - // Default (preserve whitespace) + const tokensPreserve = tokenize(line); const contentPreserve = tokensPreserve.map((t) => t.content).join(""); expect(contentPreserve).toBe(line); - // Test with a theme that has whitespace trimming enabled + const themeTrim: Theme = { name: "Trim Whitespace", schema: { @@ -74,7 +74,7 @@ describe("Tokenizer", () => { }, }; - // Just verify that tokenization works with this theme + const tokensTrim = tokenize(line, themeTrim); expect(tokensTrim).toBeInstanceOf(Array); expect(tokensTrim.length).toBeGreaterThan(0); @@ -83,12 +83,12 @@ describe("Tokenizer", () => { test("handles newlines according to theme preferences", () => { const line = "line1\nline2"; - // Default (preserve newlines) + const tokensPreserve = tokenize(line); const hasNewline = tokensPreserve.some((t) => t.content === "\n"); expect(hasNewline).toBe(true); - // Test with a theme that has newline trimming enabled + const themeTrim: Theme = { name: "Trim Newlines", schema: { @@ -96,14 +96,14 @@ describe("Tokenizer", () => { }, }; - // Just verify that tokenization works with this theme + const tokensTrim = tokenize(line, themeTrim); expect(tokensTrim).toBeInstanceOf(Array); expect(tokensTrim.length).toBeGreaterThan(0); }); test("applies theme-specific word matching", () => { - // Create a theme with a word matcher + const theme: Theme = { name: "Word Theme", schema: { @@ -113,7 +113,7 @@ describe("Tokenizer", () => { }, }; - // Create a token that should match the word + const tokens: TokenList = [ { content: "test", @@ -124,15 +124,15 @@ describe("Tokenizer", () => { }, ]; - // Apply the theme to the tokens + const styledTokens = applyTheme(tokens, theme); - // Check if the token has the style we expect + expect(styledTokens[0].metadata?.style?.color).toBe("green"); }); test("applies theme-specific pattern matching", () => { - // Create tokens manually + const tokens: TokenList = [ { content: "123", @@ -144,7 +144,7 @@ describe("Tokenizer", () => { }, ]; - // Create a theme + const theme: Theme = { name: "Simple Pattern Theme", schema: { @@ -158,18 +158,18 @@ describe("Tokenizer", () => { }, }; - // Apply the theme to the tokens + const styledTokens = applyTheme(tokens, theme); - // Check if the token has the style we expect + expect(styledTokens[0].metadata?.style?.color).toBe("blue"); }); test("handles invalid regex patterns gracefully", () => { - // Save original console.warn + const originalWarn = console.warn; - // Temporarily silence console.warn + console.warn = () => {}; try { @@ -372,4 +372,162 @@ describe("Tokenizer", () => { expect(styledTokens[0].metadata?.style).toEqual({ color: "white" }); }); }); + + describe("whitespace handling", () => { + test("handles tabs in text", () => { + const line = "text\twith\ttabs"; + const tokens = tokenize(line); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + + test("handles carriage returns", () => { + const line = "line1\r\nline2\rline3"; + const tokens = tokenize(line); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + + test("handles multiple spaces", () => { + const line = "text with multiple spaces"; + const tokens = tokenize(line); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + + test("handles tabs with trim whitespace", () => { + const theme: Theme = { + name: "Trim Tabs", + schema: { + whiteSpace: "trim", + }, + }; + const line = "text\twith\ttabs"; + const tokens = tokenize(line, theme); + expect(tokens).toBeInstanceOf(Array); + }); + + test("handles mixed whitespace characters", () => { + const line = "text \t\r\n with mixed whitespace"; + const tokens = tokenize(line); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + }); + + describe("pattern matching with identifiers", () => { + test("handles pattern matching with identifiers", () => { + const theme: Theme = { + name: "Identifier Theme", + schema: { + matchPatterns: [ + { + name: "custom-pattern", + pattern: /\b[A-Z]+\b/g, + options: { color: "yellow" }, + }, + ], + }, + }; + const line = "ERROR WARNING INFO"; + const tokens = tokenize(line, theme); + expect(tokens.length).toBeGreaterThan(0); + }); + + test("validates pattern matches correctly", () => { + const theme: Theme = { + name: "Validation Theme", + schema: { + matchPatterns: [ + { + name: "strict-match", + pattern: /^\d{4}-\d{2}-\d{2}$/, + options: { color: "green" }, + }, + ], + }, + }; + const line = "2024-01-15 is a date"; + const tokens = tokenize(line, theme); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + }); + + describe("complex scenarios", () => { + test("handles empty strings", () => { + const line = ""; + const tokens = tokenize(line); + expect(tokens.length).toBeGreaterThanOrEqual(0); + }); + + test("handles strings with only whitespace", () => { + const line = " \t \n "; + const tokens = tokenize(line); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + + test("handles unicode characters", () => { + const line = "Hello 世界 🌍"; + const tokens = tokenize(line); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + + test("handles very long lines", () => { + const line = "a".repeat(10000); + const tokens = tokenize(line); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + + test("handles theme with both word and pattern matches", () => { + const theme: Theme = { + name: "Combined Theme", + schema: { + matchWords: { + ERROR: { color: "red" }, + INFO: { color: "blue" }, + }, + matchPatterns: [ + { + name: "timestamp", + pattern: /\d{2}:\d{2}:\d{2}/g, + options: { color: "gray" }, + }, + ], + }, + }; + const line = "12:00:00 ERROR Something went wrong INFO Check logs"; + const tokens = tokenize(line, theme); + expect(tokens.length).toBeGreaterThan(0); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + + test("handles overlapping patterns", () => { + const theme: Theme = { + name: "Overlap Theme", + schema: { + matchPatterns: [ + { + name: "word", + pattern: /\w+/g, + options: { color: "blue" }, + }, + { + name: "number", + pattern: /\d+/g, + options: { color: "green" }, + }, + ], + }, + }; + const line = "test123 word456"; + const tokens = tokenize(line, theme); + const content = tokens.map((t) => t.content).join(""); + expect(content).toBe(line); + }); + }); }); diff --git a/tests/unit/utils/logger.test.ts b/tests/unit/utils/logger.test.ts index 8358198..e1eb49c 100644 --- a/tests/unit/utils/logger.test.ts +++ b/tests/unit/utils/logger.test.ts @@ -3,7 +3,7 @@ import { logger } from "../../../src/utils/logger"; describe("logger", () => { afterEach(() => { - // Restore all spies after each test + }); test("info() logs with blue info icon", () => { diff --git a/tests/unit/utils/spinner.test.ts b/tests/unit/utils/spinner.test.ts index 9d0dc67..bf60985 100644 --- a/tests/unit/utils/spinner.test.ts +++ b/tests/unit/utils/spinner.test.ts @@ -94,8 +94,8 @@ describe("spinner", () => { s.stop(); const output = mockWrites.join(""); - expect(output).toContain("\x1B[K"); // Clear line - expect(output).toContain("\x1B[?25h"); // Show cursor + expect(output).toContain("\x1B[K"); + expect(output).toContain("\x1B[?25h"); }); test("start should hide cursor and write frames", async () => { @@ -106,7 +106,7 @@ describe("spinner", () => { s.stop(); const output = mockWrites.join(""); - expect(output).toContain("\x1B[?25l"); // Hide cursor + expect(output).toContain("\x1B[?25l"); expect(output).toContain("Test"); }); From c9e0dd2c82a771e77a29807bc8d80a9679e8b65e Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:13:36 -0800 Subject: [PATCH 2/6] chore: test improvements v2 --- src/cli/commands/theme.ts | 9 --- src/cli/interactive.ts | 3 - src/cli/theme/generator.ts | 9 +-- src/cli/theme/transporter.ts | 11 +-- src/cli/types.ts | 2 - src/cli/utils.ts | 19 ----- src/index.ts | 90 +----------------------- src/renderer/constants.ts | 15 ---- src/renderer/detectBackground.ts | 33 --------- src/renderer/index.ts | 81 --------------------- src/renderer/lightBox.ts | 29 -------- src/schema/validator.ts | 10 --- src/themes/color-constants.ts | 4 -- src/themes/index.ts | 17 ----- src/themes/template.ts | 2 - src/themes/utils.ts | 24 +------ src/tokenizer/index.ts | 17 ----- src/types.ts | 25 ++----- src/utils/ascii.ts | 2 - src/utils/boxen.ts | 7 -- src/utils/colors.ts | 6 -- src/utils/gradient.ts | 1 - src/utils/spinner.ts | 10 +-- tests/unit/cli/index.test.ts | 2 - tests/unit/cli/theme/generator.test.ts | 6 +- tests/unit/cli/theme/transporter.test.ts | 11 --- tests/unit/index.test.ts | 34 ++------- tests/unit/renderer/constants.test.ts | 1 - tests/unit/renderer/index.test.ts | 82 ++++++++++++++++++--- tests/unit/schema/validator.test.ts | 10 +-- tests/unit/themes/index.test.ts | 5 -- tests/unit/themes/template.test.ts | 8 +-- tests/unit/themes/utils.test.ts | 6 +- tests/unit/tokenizer/index.test.ts | 21 ------ tests/unit/utils/logger.test.ts | 4 +- tests/unit/utils/spinner.test.ts | 6 +- 36 files changed, 105 insertions(+), 517 deletions(-) diff --git a/src/cli/commands/theme.ts b/src/cli/commands/theme.ts index bc966e7..ad0b158 100644 --- a/src/cli/commands/theme.ts +++ b/src/cli/commands/theme.ts @@ -16,7 +16,6 @@ import { registerTheme, getTheme, getThemeNames } from "../../themes"; import { getLogsDX } from "../../index"; import { Theme } from "../../types"; - const SAMPLE_LOGS = [ "INFO: Server started on port 3000", "WARN: Memory usage high: 85%", @@ -28,7 +27,6 @@ const SAMPLE_LOGS = [ "Cache hit ratio: 92.5%", ]; - const COLOR_PRESETS = { Vibrant: { primary: "#007acc", @@ -105,7 +103,6 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { showBanner(); } - const name = await input({ message: "Theme name:", validate: (inputValue: string) => { @@ -138,7 +135,6 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { mode, }; - const preset = await select({ message: "Choose a color preset:", choices: Object.keys(COLOR_PRESETS).map((name) => ({ @@ -211,7 +207,6 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { colors = { primary, error, warning, success, info, muted }; } - const presets = await checkbox({ message: "Select features to highlight:", choices: [ @@ -236,7 +231,6 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { ], }); - const createSpinner = spinner("Creating theme...").start(); const config: SimpleThemeConfig = { @@ -250,11 +244,9 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { const theme = createTheme(config); createSpinner.succeed("Theme created!"); - console.log("\n"); renderPreview(theme, `✨ ${theme.name} Preview`); - const checkAccessibility = await confirm({ message: "Check accessibility compliance?", default: true, @@ -301,7 +293,6 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) { } } - const saveOption = await select({ message: "How would you like to save the theme?", choices: [ diff --git a/src/cli/interactive.ts b/src/cli/interactive.ts index ec356aa..b28de4f 100644 --- a/src/cli/interactive.ts +++ b/src/cli/interactive.ts @@ -39,7 +39,6 @@ export async function runInteractiveMode(): Promise { ), ); - const themeNames = getThemeNames(); const themeChoices: ThemeChoice[] = themeNames.map((name: string) => ({ name: chalk.cyan(name), @@ -79,7 +78,6 @@ export async function runInteractiveMode(): Promise { }); } - const outputFormat = await select({ message: "📤 Choose output format:", choices: [ @@ -96,7 +94,6 @@ export async function runInteractiveMode(): Promise { ], }); - const wantPreview = await confirm({ message: "👀 Show a preview with your settings?", default: true, diff --git a/src/cli/theme/generator.ts b/src/cli/theme/generator.ts index 36ee592..ef4a394 100644 --- a/src/cli/theme/generator.ts +++ b/src/cli/theme/generator.ts @@ -391,21 +391,18 @@ export function validateColorInput(color: string): boolean | string { return false; } - if (color.match(/^[0-9a-fA-F]+$/)) { - return false; + return false; } if (color.startsWith("#")) { return hexPattern.test(color); } - if (color.startsWith("rgb")) { return rgbPattern.test(color); } - return namedColors.includes(color.toLowerCase()); } @@ -456,7 +453,6 @@ interface ThemeAnswers { } export function generateTemplateFromAnswers(answers: ThemeAnswers): Theme { - const patternPresets = answers.patterns || answers.patternPresets || []; if (answers.features && answers.features.includes("logLevels")) { patternPresets.push("log-levels"); @@ -478,12 +474,10 @@ export function generateTemplateFromAnswers(answers: ThemeAnswers): Theme { theme.mode = answers.mode as Theme["mode"]; } - if (!theme.schema) { theme.schema = {}; } - if (answers.features) { if ( answers.features.includes("numbers") || @@ -495,7 +489,6 @@ export function generateTemplateFromAnswers(answers: ThemeAnswers): Theme { theme.schema.matchWords.null = { color: "#808080" }; } if (answers.features.includes("brackets")) { - theme.schema.matchStartsWith = theme.schema.matchStartsWith || {}; theme.schema.matchEndsWith = theme.schema.matchEndsWith || {}; theme.schema.matchStartsWith["["] = { color: "#ffff00" }; diff --git a/src/cli/theme/transporter.ts b/src/cli/theme/transporter.ts index 7d11ea2..71d1df5 100644 --- a/src/cli/theme/transporter.ts +++ b/src/cli/theme/transporter.ts @@ -100,8 +100,6 @@ export function importThemeFromFile(filePath: string): Theme { const fileContent = fs.readFileSync(filePath, "utf8"); if (filePath.endsWith(".ts") || filePath.endsWith(".js")) { - - const patterns = [ /export\s+const\s+\w+\s*:\s*\w+\s*=\s*(\{[\s\S]*?\})\s*;?\s*$/m, /export\s+default\s+(\{[\s\S]*?\})\s*;?\s*$/m, @@ -112,10 +110,9 @@ export function importThemeFromFile(filePath: string): Theme { const match = fileContent.match(pattern); if (match) { try { - const jsonStr = match[1] - .replace(/^\s+/gm, "") - .replace(/\s+$/gm, "") + .replace(/^\s+/gm, "") + .replace(/\s+$/gm, "") .trim(); return JSON.parse(jsonStr); } catch { @@ -159,7 +156,6 @@ export async function importTheme(filename?: string): Promise { const fileContent = fs.readFileSync(themeFile, "utf8"); const themeData = JSON.parse(fileContent); - const validatedTheme = themePresetSchema.parse(themeData); ui.showInfo(`Importing theme: ${chalk.cyan(validatedTheme.name)}`); @@ -167,7 +163,6 @@ export async function importTheme(filename?: string): Promise { console.log(`Description: ${validatedTheme.description}`); } - const existingTheme = getTheme(validatedTheme.name); if (existingTheme) { const shouldOverwrite = await confirm({ @@ -185,7 +180,6 @@ export async function importTheme(filename?: string): Promise { } } - const showPreview = await confirm({ message: "Preview theme before importing?", default: true, @@ -244,7 +238,6 @@ async function previewImportedTheme(theme: Theme) { "POST /api/login 401 Unauthorized 23ms", ]; - const { LogsDX } = await import("../../index"); registerTheme(theme); const logsDX = LogsDX.getInstance({ diff --git a/src/cli/types.ts b/src/cli/types.ts index a700728..b852979 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -13,7 +13,6 @@ export const cliOptionsSchema = z.object({ noSpinner: z.boolean().optional().default(false), format: z.enum(["ansi", "html"]).optional(), - generateTheme: z.boolean().optional().default(false), listPalettes: z.boolean().optional().default(false), listPatterns: z.boolean().optional().default(false), @@ -25,7 +24,6 @@ export const cliOptionsSchema = z.object({ export type CliOptions = z.infer; export type CommanderOptions = CliOptions; - export interface SpinnerLike { start(): this; succeed(message?: string): this; diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 5f65d50..a2925b6 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -1,10 +1,5 @@ import { SIZE_UNITS, SIZE_UNIT_MULTIPLIER } from "./constants"; - - - - - export function formatFileSize(bytes: number): string { if (bytes === 0) { return "0 B"; @@ -20,20 +15,10 @@ export function formatFileSize(bytes: number): string { return `${size.toFixed(1)} ${unit}`; } - - - - - export function formatNumber(num: number): string { return num.toLocaleString(); } - - - - - export function fileExists(path: string): boolean { try { return require("fs").existsSync(path); @@ -42,10 +27,6 @@ export function fileExists(path: string): boolean { } } - - - - export function ensureDir(dirPath: string): void { const fs = require("fs"); if (!fs.existsSync(dirPath)) { diff --git a/src/index.ts b/src/index.ts index c531806..c8c592a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,10 +50,6 @@ export class LogsDX { }, }; - - - - private constructor(options = {}) { this.options = { theme: "none", @@ -69,11 +65,6 @@ export class LogsDX { this.currentTheme = this.resolveTheme(this.options.theme); } - - - - - private resolveTheme(theme: string | Theme | ThemePair | undefined): Theme { if (!theme || theme === "none") { return { @@ -147,14 +138,13 @@ export class LogsDX { } } } else { - try { return validateTheme(theme as Theme); } catch (error) { if (this.options.debug) { console.warn("Invalid custom theme:", error); } - + return { name: "none", description: "No styling applied", @@ -172,22 +162,15 @@ export class LogsDX { } } - - - - - static getInstance(options: LogsDXOptions = {}): LogsDX { if (!LogsDX.instance) { LogsDX.instance = new LogsDX(options); } else if (Object.keys(options).length > 0) { - LogsDX.instance.options = { ...LogsDX.instance.options, ...options, }; - if (options.theme) { LogsDX.instance.currentTheme = LogsDX.instance.resolveTheme( options.theme, @@ -201,11 +184,6 @@ export class LogsDX { LogsDX.instance = null; } - - - - - processLine(line: string): string { const renderOptions: RenderOptions = { theme: this.currentTheme, @@ -214,13 +192,10 @@ export class LogsDX { escapeHtml: this.options.escapeHtml, }; - const tokens = tokenize(line, this.currentTheme); - const styledTokens = applyTheme(tokens, this.currentTheme); - if (renderOptions.outputFormat === "html") { if (renderOptions.htmlStyleFormat === "className") { return tokensToClassNames(styledTokens); @@ -228,45 +203,24 @@ export class LogsDX { return tokensToHtml(styledTokens); } } else { - return tokensToString(styledTokens); } } - - - - - processLines(lines: string[]): string[] { return lines.map((line) => this.processLine(line)); } - - - - - processLog(logContent: string): string { const lines = logContent.split("\n"); const processedLines = this.processLines(lines); return processedLines.join("\n"); } - - - - - tokenizeLine(line: string): TokenList { return tokenize(line, this.currentTheme); } - - - - - setTheme(theme: string | Theme | ThemePair): boolean { try { this.options.theme = theme; @@ -280,77 +234,35 @@ export class LogsDX { } } - - - - getCurrentTheme(): Theme { return this.currentTheme; } - - - - getAllThemes(): Record { return getAllThemes(); } - - - - getThemeNames(): string[] { return getThemeNames(); } - - - - setOutputFormat(format: "ansi" | "html"): void { this.options.outputFormat = format; } - - - - setHtmlStyleFormat(format: "css" | "className"): void { this.options.htmlStyleFormat = format; } - - - - getCurrentOutputFormat(): "ansi" | "html" { return this.options.outputFormat; } - - - - getCurrentHtmlStyleFormat(): "css" | "className" { return this.options.htmlStyleFormat; } } - - - - - - - - - - - - - - export function getLogsDX(options?: LogsDXOptions): LogsDX { return LogsDX.getInstance(options); } diff --git a/src/renderer/constants.ts b/src/renderer/constants.ts index 9ff70ce..f20aa52 100644 --- a/src/renderer/constants.ts +++ b/src/renderer/constants.ts @@ -41,10 +41,6 @@ export const CLASS_ITALIC = "logsdx-italic"; export const CLASS_UNDERLINE = "logsdx-underline"; export const CLASS_DIM = "logsdx-dim"; - - - - export function supportsColors(): boolean { if (process.env.NO_COLOR) { return false; @@ -181,12 +177,6 @@ export const TEXT_COLORS: Record = { }, }; - - - - - - export function getColorDefinition( colorName: string, theme?: Theme, @@ -238,11 +228,6 @@ export function getColorDefinition( return undefined; } - - - - - function hexToRgb(hex: string): [number, number, number] { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result diff --git a/src/renderer/detectBackground.ts b/src/renderer/detectBackground.ts index e4f59e9..8f7897a 100644 --- a/src/renderer/detectBackground.ts +++ b/src/renderer/detectBackground.ts @@ -88,10 +88,6 @@ function detectFromVSCode(): BackgroundInfo | undefined { }); } - - - - export function detectTerminalBackground(): BackgroundInfo { const fromColorFgBg = detectFromColorFgBg(); if (fromColorFgBg) { @@ -120,10 +116,6 @@ function matchesColorScheme(scheme: "dark" | "light"): boolean { return query.matches; } - - - - export function detectBrowserBackground(): BackgroundInfo { if (!hasMatchMedia()) { return DEFAULT_AUTO_BACKGROUND; @@ -188,10 +180,6 @@ function detectFromLinux(): BackgroundInfo | undefined { }); } - - - - export function detectSystemBackground(): BackgroundInfo { const fromMacOS = detectFromMacOS(); if (fromMacOS) { @@ -221,10 +209,6 @@ function hasHigherConfidence(a: ConfidenceLevel, b: ConfidenceLevel): boolean { return confidenceOrder[a] >= confidenceOrder[b]; } - - - - export function detectBackground(): BackgroundInfo { if (isBrowser()) { const browserInfo = detectBrowserBackground(); @@ -251,10 +235,6 @@ export function detectBackground(): BackgroundInfo { : systemInfo; } - - - - export function isDarkBackground(): boolean { const info = detectBackground(); const isDefaultAuto = info.scheme === "auto" && info.source === "default"; @@ -262,19 +242,11 @@ export function isDarkBackground(): boolean { return info.scheme === "dark" || isDefaultAuto; } - - - - export function isLightBackground(): boolean { const info = detectBackground(); return info.scheme === "light"; } - - - - export function getRecommendedThemeMode(): "light" | "dark" { return isDarkBackground() ? "dark" : "light"; } @@ -316,11 +288,6 @@ function setupMediaQueryListeners( return () => {}; } - - - - - export function watchBackgroundChanges( callback: (info: BackgroundInfo) => void, ): () => void { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 4e082b0..61ae8a3 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -109,12 +109,6 @@ export function tokenToString(token: Token, colorSupport: boolean): string { return result; } - - - - - - export function tokensToString( tokens: TokenList, forceColors?: boolean, @@ -229,12 +223,6 @@ export function tokenToHtml(token: Token, options: RenderOptions): string { return wrapInSpan(content, css); } - - - - - - export function tokensToHtml( tokens: TokenList, options: RenderOptions = {}, @@ -306,12 +294,6 @@ export function tokenToClassName(token: Token, options: RenderOptions): string { return wrapInSpanWithClass(content, classes); } - - - - - - export function tokensToClassNames( tokens: TokenList, options: RenderOptions = {}, @@ -321,13 +303,6 @@ export function tokensToClassNames( .join(EMPTY_STRING); } - - - - - - - export function renderLine( line: string, theme?: Theme, @@ -346,21 +321,10 @@ export function renderLine( return tokensToString(styledTokens); } - - - - - export function highlightLine(line: string): string { return `${LINE_HIGHLIGHT_BG}${line}${RESET}`; } - - - - - - export function applyColor(text: string, color: string): string { const colorDef = getColorDefinition(color); if (!colorDef) { @@ -369,48 +333,22 @@ export function applyColor(text: string, color: string): string { return `${colorDef.ansi}${text}${STYLE_CODES.resetColor}`; } - - - - - export function applyBold(text: string): string { return `${STYLE_CODES.bold}${text}${STYLE_CODES.resetBold}`; } - - - - - export function applyItalic(text: string): string { return `${STYLE_CODES.italic}${text}${STYLE_CODES.resetItalic}`; } - - - - - export function applyUnderline(text: string): string { return `${STYLE_CODES.underline}${text}${STYLE_CODES.resetUnderline}`; } - - - - - export function applyDim(text: string): string { return `${STYLE_CODES.dim}${text}${STYLE_CODES.resetDim}`; } - - - - - - export function applyBackgroundColor(text: string, color: string): string { const colorDef = BACKGROUND_COLORS[color]; if (!colorDef) { @@ -419,33 +357,14 @@ export function applyBackgroundColor(text: string, color: string): string { return `${colorDef.ansi}${text}${STYLE_CODES.resetBackground}`; } - - - - - export function fg256(code: number): string { return `\x1b[38;5;${code}m`; } - - - - - - - export function fgRGB(r: number, g: number, b: number): string { return `\x1b[38;2;${r};${g};${b}m`; } - - - - - - - export function renderLines( lines: ReadonlyArray, theme?: Theme, diff --git a/src/renderer/lightBox.ts b/src/renderer/lightBox.ts index cae8bff..0cb4429 100644 --- a/src/renderer/lightBox.ts +++ b/src/renderer/lightBox.ts @@ -52,11 +52,6 @@ const DEFAULT_PADDING = 2; const DEFAULT_BORDER = true; const DEFAULT_BORDER_STYLE: BorderStyle = "rounded"; - - - - - export function getThemeBackground(theme: Theme | string): string { const themeName = typeof theme === "string" ? theme : theme.name; return THEME_BACKGROUNDS[themeName] || DEFAULT_BACKGROUND; @@ -132,13 +127,6 @@ function createPaddedLine( return `${backgroundColor}${content}${RESET}`; } - - - - - - - export function renderLightBoxLine( line: string, theme: Theme | string, @@ -163,14 +151,6 @@ export function renderLightBoxLine( ); } - - - - - - - - export function renderLightBox( lines: ReadonlyArray, theme: Theme | string, @@ -199,11 +179,6 @@ export function renderLightBox( return result; } - - - - - export function isLightTheme(theme: Theme | string): boolean { if (typeof theme === "object" && theme.mode) { return theme.mode === "light"; @@ -215,10 +190,6 @@ export function isLightTheme(theme: Theme | string): boolean { return lowerName.includes("light") || lowerName.includes("white"); } - - - - export function isTerminalDark(): boolean { return isDarkBackground(); } diff --git a/src/schema/validator.ts b/src/schema/validator.ts index 7357a1c..d2ff437 100644 --- a/src/schema/validator.ts +++ b/src/schema/validator.ts @@ -91,11 +91,6 @@ export function createThemeValidationError(error: unknown): Error { return createValidationError(message, error); } - - - - - export function validateTheme(theme: unknown): Theme { try { return parseTheme(theme); @@ -104,11 +99,6 @@ export function validateTheme(theme: unknown): Theme { } } - - - - - export function validateThemeSafe(theme: unknown): { success: boolean; data?: Theme; diff --git a/src/themes/color-constants.ts b/src/themes/color-constants.ts index 25922ff..12f7d82 100644 --- a/src/themes/color-constants.ts +++ b/src/themes/color-constants.ts @@ -1,7 +1,3 @@ - - - - export const colors = { gray: { 50: "#f9fafb", 100: "#f3f4f6", 800: "#1f2937", 900: "#111827" }, sky: { 300: "#7dd3fc", 400: "#38bdf8", 600: "#0284c7", 700: "#0369a1" }, diff --git a/src/themes/index.ts b/src/themes/index.ts index 8877007..b01d2a8 100644 --- a/src/themes/index.ts +++ b/src/themes/index.ts @@ -11,35 +11,18 @@ import { } from "./builder"; import type { ColorPalette, SimpleThemeConfig } from "./builder"; - - - - - export function getTheme(themeName: string): Theme { return THEMES[themeName] || (THEMES[DEFAULT_THEME] as Theme); } - - - - export function getAllThemes(): Record { return THEMES; } - - - - export function getThemeNames(): string[] { return Object.keys(THEMES); } - - - - export function registerTheme(theme: Theme): void { THEMES[theme.name] = theme; } diff --git a/src/themes/template.ts b/src/themes/template.ts index 57dca40..f2a38d6 100644 --- a/src/themes/template.ts +++ b/src/themes/template.ts @@ -129,7 +129,6 @@ export const themeGeneratorConfigSchema = z.object({ export type ThemeGeneratorConfig = z.infer; - export const COLOR_PALETTES: ColorPalette[] = [ { name: "github-light", @@ -254,7 +253,6 @@ export const COLOR_PALETTES: ColorPalette[] = [ }, ]; - export const PATTERN_PRESETS: PatternPreset[] = [ { name: "http-api", diff --git a/src/themes/utils.ts b/src/themes/utils.ts index 7d7a9fc..aea1aa5 100644 --- a/src/themes/utils.ts +++ b/src/themes/utils.ts @@ -1,31 +1,17 @@ - - - - import { colors } from "./color-constants"; - - - export function isDarkColor(hex: string): boolean { - const color = hex.replace("#", ""); - const r = parseInt(color.slice(0, 2), 16); const g = parseInt(color.slice(2, 4), 16); const b = parseInt(color.slice(4, 6), 16); - const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance < 0.5; } - - - - export function getAccessibleTextColors( backgroundColor: string, contrastLevel: "AAA" | "AA" = "AA", @@ -33,7 +19,6 @@ export function getAccessibleTextColors( const isDark = isDarkColor(backgroundColor); if (isDark) { - return { text: contrastLevel === "AAA" ? colors.gray[50] : colors.gray[100], info: contrastLevel === "AAA" ? colors.sky[300] : colors.sky[400], @@ -45,7 +30,6 @@ export function getAccessibleTextColors( string: contrastLevel === "AAA" ? colors.lime[300] : colors.lime[400], }; } else { - return { text: contrastLevel === "AAA" ? colors.gray[900] : colors.gray[800], info: contrastLevel === "AAA" ? colors.sky[700] : colors.sky[600], @@ -59,9 +43,6 @@ export function getAccessibleTextColors( } } - - - export function getWCAGLevel( ratio: number, isLargeText: boolean = false, @@ -73,14 +54,11 @@ export function getWCAGLevel( } else { if (ratio >= 7) return "AAA"; if (ratio >= 4.5) return "AA"; - if (ratio >= 3) return "A"; + if (ratio >= 3) return "A"; return "FAIL"; } } - - - export function getWCAGRecommendations(ratio: number): string[] { const recommendations: string[] = []; diff --git a/src/tokenizer/index.ts b/src/tokenizer/index.ts index aff8811..5c3d15f 100644 --- a/src/tokenizer/index.ts +++ b/src/tokenizer/index.ts @@ -354,11 +354,6 @@ export function addThemeRules(lexer: SimpleLexer, theme: Theme): void { } } - - - - - export function createLexer(theme?: Theme): SimpleLexer { const lexer = new SimpleLexer(); @@ -465,12 +460,6 @@ export function convertLexerTokens( return lexerTokens.map(convertLexerToken); } - - - - - - export function tokenize(line: string, theme?: Theme): TokenList { try { if (shouldUseDefaultToken(theme)) { @@ -643,12 +632,6 @@ export function applyTokenStyle(token: Token, theme: Theme): Token { return applyDefaultStyle(token, theme); } - - - - - - export function applyTheme(tokens: TokenList, theme: Theme): TokenList { if (!theme || !theme.schema) { return tokens; diff --git a/src/types.ts b/src/types.ts index 706fd4d..b6b0be7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,3 @@ - - - export type StyleCode = | "bold" | "italic" @@ -10,21 +7,14 @@ export type StyleCode = | "reverse" | "strikethrough"; - - - export interface StyleOptions { - color: string; - + styleCodes?: StyleCode[]; - + htmlStyleFormat?: "css" | "className"; } - - - export function filterStyleCodes(codes: string[] | undefined): StyleCode[] { if (!codes) return []; const validCodes: StyleCode[] = [ @@ -81,14 +71,9 @@ export interface Theme { export type ThemePreset = Theme; - - - - export interface ThemePair { - light: string | Theme; - + dark: string | Theme; } @@ -96,10 +81,10 @@ export interface LogsDXOptions { theme?: string | Theme | ThemePair; outputFormat?: "ansi" | "html"; htmlStyleFormat?: "css" | "className"; - escapeHtml?: boolean; + escapeHtml?: boolean; debug?: boolean; customRules?: Record; - autoAdjustTerminal?: boolean; + autoAdjustTerminal?: boolean; } export type LogLevel = "debug" | "info" | "warn" | "error"; diff --git a/src/utils/ascii.ts b/src/utils/ascii.ts index aa09fe7..3a16161 100644 --- a/src/utils/ascii.ts +++ b/src/utils/ascii.ts @@ -12,12 +12,10 @@ export function textSync( verticalLayout?: string; }, ): string { - if (text === "LogsDX") { return LOGSDX_ASCII; } - return text; } diff --git a/src/utils/boxen.ts b/src/utils/boxen.ts index d359fc8..0edf338 100644 --- a/src/utils/boxen.ts +++ b/src/utils/boxen.ts @@ -84,14 +84,12 @@ export function boxen(text: string, options: BoxenOptions = {}): string { const result: string[] = []; - for (let i = 0; i < margin.top; i++) { result.push(""); } const leftMargin = " ".repeat(margin.left); - const topBorder = options.title ? border.topLeft + ` ${options.title} ` + @@ -102,14 +100,12 @@ export function boxen(text: string, options: BoxenOptions = {}): string { : border.topLeft + border.horizontal.repeat(boxWidth) + border.topRight; result.push(leftMargin + topBorder); - for (let i = 0; i < padding.top; i++) { result.push( leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical, ); } - lines.forEach((line) => { const cleanLength = line.replace(/\x1B\[[0-9;]*m/g, "").length; const paddingRight = " ".repeat(Math.max(0, contentWidth - cleanLength)); @@ -124,14 +120,12 @@ export function boxen(text: string, options: BoxenOptions = {}): string { ); }); - for (let i = 0; i < padding.bottom; i++) { result.push( leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical, ); } - result.push( leftMargin + border.bottomLeft + @@ -139,7 +133,6 @@ export function boxen(text: string, options: BoxenOptions = {}): string { border.bottomRight, ); - for (let i = 0; i < margin.bottom; i++) { result.push(""); } diff --git a/src/utils/colors.ts b/src/utils/colors.ts index a87092f..73e128c 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -1,5 +1,4 @@ const styles = { - black: "\x1B[30m", red: "\x1B[31m", green: "\x1B[32m", @@ -10,7 +9,6 @@ const styles = { white: "\x1B[37m", gray: "\x1B[90m", - redBright: "\x1B[91m", greenBright: "\x1B[92m", yellowBright: "\x1B[93m", @@ -19,13 +17,11 @@ const styles = { cyanBright: "\x1B[96m", whiteBright: "\x1B[97m", - bold: "\x1B[1m", dim: "\x1B[2m", italic: "\x1B[3m", underline: "\x1B[4m", - reset: "\x1B[0m", }; @@ -41,7 +37,6 @@ function createChainableColor(appliedStyles: string[] = []): any { return `${prefix}${text}${styles.reset}`; }; - Object.keys(styles).forEach((key) => { if (key === "reset") return; Object.defineProperty(fn, key, { @@ -59,7 +54,6 @@ function createChainableColor(appliedStyles: string[] = []): any { export const colors = createChainableColor(); - export const red = createColorFunction(styles.red); export const green = createColorFunction(styles.green); export const yellow = createColorFunction(styles.yellow); diff --git a/src/utils/gradient.ts b/src/utils/gradient.ts index 7cd1c8f..40ee06e 100644 --- a/src/utils/gradient.ts +++ b/src/utils/gradient.ts @@ -2,7 +2,6 @@ export function gradient(colors: string[]): { (text: string): string; multiline(text: string): string; } { - const applyGradient = (text: string) => `\x1B[36m${text}\x1B[0m`; applyGradient.multiline = (text: string) => { diff --git a/src/utils/spinner.ts b/src/utils/spinner.ts index bca4f6b..b4bb0f9 100644 --- a/src/utils/spinner.ts +++ b/src/utils/spinner.ts @@ -1,7 +1,3 @@ - - - - export interface Spinner { start(): Spinner; succeed(text?: string): Spinner; @@ -30,7 +26,7 @@ export function spinner(initialText: string): Spinner { if (isSpinning) return instance; isSpinning = true; - process.stdout.write("\x1B[?25l"); + process.stdout.write("\x1B[?25l"); interval = setInterval(() => { const frame = frames[frameIndex]; @@ -61,8 +57,8 @@ export function spinner(initialText: string): Spinner { interval = null; } if (isSpinning) { - process.stdout.write("\r\x1B[K"); - process.stdout.write("\x1B[?25h"); + process.stdout.write("\r\x1B[K"); + process.stdout.write("\x1B[?25h"); isSpinning = false; } return instance; diff --git a/tests/unit/cli/index.test.ts b/tests/unit/cli/index.test.ts index e7338be..e63d619 100644 --- a/tests/unit/cli/index.test.ts +++ b/tests/unit/cli/index.test.ts @@ -5,8 +5,6 @@ import fs from "fs"; import os from "os"; import path from "path"; - - describe("parseArgs", () => { test("should parse basic theme argument", () => { const args = ["--theme", "dracula"]; diff --git a/tests/unit/cli/theme/generator.test.ts b/tests/unit/cli/theme/generator.test.ts index 366cc4f..cb85d1f 100644 --- a/tests/unit/cli/theme/generator.test.ts +++ b/tests/unit/cli/theme/generator.test.ts @@ -22,13 +22,13 @@ describe("Theme Generator", () => { it("should validate rgb colors", () => { expect(validateColorInput("rgb(255, 0, 0)")).toBe(true); expect(validateColorInput("rgba(255, 0, 0, 0.5)")).toBe(true); - expect(validateColorInput("rgb(256, 0, 0)")).toBe(true); + expect(validateColorInput("rgb(256, 0, 0)")).toBe(true); }); it("should validate named colors", () => { expect(validateColorInput("red")).toBe(true); expect(validateColorInput("blue")).toBe(true); - expect(validateColorInput("notacolor")).toBe(true); + expect(validateColorInput("notacolor")).toBe(true); }); }); @@ -120,11 +120,9 @@ describe("Theme Generator", () => { expect(theme.schema.matchWords).toHaveProperty("false"); expect(theme.schema.matchWords).toHaveProperty("null"); - expect(theme.schema.matchStartsWith).toBeDefined(); expect(theme.schema.matchEndsWith).toBeDefined(); - expect(theme.schema.matchStartsWith?.["["]).toBeDefined(); expect(theme.schema.matchEndsWith?.["]"]).toBeDefined(); }); diff --git a/tests/unit/cli/theme/transporter.test.ts b/tests/unit/cli/theme/transporter.test.ts index d3ee698..30a3a02 100644 --- a/tests/unit/cli/theme/transporter.test.ts +++ b/tests/unit/cli/theme/transporter.test.ts @@ -8,19 +8,16 @@ import { } from "../../../../src/cli/theme/transporter"; import type { Theme } from "../../../../src/types"; - const TEST_DIR = join(process.cwd(), ".test-themes"); describe("Theme Transporter", () => { beforeEach(() => { - if (!existsSync(TEST_DIR)) { mkdirSync(TEST_DIR, { recursive: true }); } }); afterEach(() => { - if (existsSync(TEST_DIR)) { rmSync(TEST_DIR, { recursive: true, force: true }); } @@ -121,7 +118,6 @@ describe("Theme Transporter", () => { it("should validate imported theme", () => { const filePath = join(TEST_DIR, "invalid-theme.json"); const invalidTheme = { - description: "Invalid theme", }; writeFileSync(filePath, JSON.stringify(invalidTheme, null, 2)); @@ -144,7 +140,6 @@ describe("Theme Transporter", () => { describe("listThemeFiles", () => { it("should list theme files in directory", () => { - writeFileSync(join(TEST_DIR, "theme1.json"), JSON.stringify(sampleTheme)); writeFileSync(join(TEST_DIR, "theme2.json"), JSON.stringify(sampleTheme)); writeFileSync( @@ -195,26 +190,20 @@ describe("Theme Transporter", () => { it("should round-trip theme through export and import", () => { const filePath = join(TEST_DIR, "round-trip.json"); - exportThemeToFile(sampleTheme, filePath, "json"); - const imported = importThemeFromFile(filePath); - expect(imported).toEqual(sampleTheme); }); it("should handle TypeScript round-trip", () => { const filePath = join(TEST_DIR, "round-trip.ts"); - exportThemeToFile(sampleTheme, filePath, "typescript"); - const imported = importThemeFromFile(filePath); - expect(imported.name).toBe(sampleTheme.name); expect(imported.description).toBe(sampleTheme.description); expect(imported.mode).toBe(sampleTheme.mode); diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index c0df4a8..9e7248b 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -3,11 +3,9 @@ import stripAnsi from "strip-ansi"; import { LogsDX, getLogsDX } from "../../src/index"; describe("LogsDX", () => { - const originalConsoleWarn = console.warn; let consoleWarnings: string[] = []; - beforeEach(() => { LogsDX.resetInstance(); consoleWarnings = []; @@ -33,9 +31,8 @@ describe("LogsDX", () => { }); test("applies custom options when provided", () => { - LogsDX.resetInstance(); - + const customInstance = LogsDX.getInstance({ theme: "oh-my-zsh" }); expect(customInstance.getCurrentTheme().name).toBe("oh-my-zsh"); }); @@ -64,7 +61,6 @@ describe("LogsDX", () => { const line = "test line"; const result = instance.processLine(line); - expect(typeof result).toBe("string"); }); @@ -108,7 +104,6 @@ describe("LogsDX", () => { expect(tokens).toBeInstanceOf(Array); expect(tokens.length).toBeGreaterThan(0); - const totalContent = tokens.map((t) => t.content).join(""); expect(totalContent).toBe("test line"); }); @@ -117,7 +112,7 @@ describe("LogsDX", () => { describe("setTheme", () => { test("sets theme by name", () => { const instance = LogsDX.getInstance(); - + const result = instance.setTheme("oh-my-zsh"); expect(result).toBe(true); expect(instance.getCurrentTheme().name).toBe("oh-my-zsh"); @@ -138,9 +133,9 @@ describe("LogsDX", () => { test("returns true even for invalid theme (fails silently)", () => { const instance = LogsDX.getInstance({ debug: true }); - + expect(instance.setTheme({ invalid: "theme" })).toBe(true); - + expect(instance.getCurrentTheme().name).toBe("none"); }); }); @@ -199,57 +194,45 @@ describe("LogsDX", () => { }); describe("ANSI theming integration", () => { - const originalConsoleWarn = console.warn; beforeEach(() => { - LogsDX.resetInstance(); - + console.warn = () => {}; }); afterEach(() => { - console.warn = originalConsoleWarn; }); test("applies theme colors to ANSI output", () => { - const instance = LogsDX.getInstance({ theme: "oh-my-zsh", - outputFormat: "ansi", + outputFormat: "ansi", }); - expect(instance.getCurrentOutputFormat()).toBe("ansi"); - const errorLine = instance.processLine("ERROR: This is an error message"); const warnLine = instance.processLine("WARN: This is a warning message"); const infoLine = instance.processLine("INFO: This is an info message"); - expect(stripAnsi(errorLine)).toBe("ERROR: This is an error message"); expect(stripAnsi(warnLine)).toBe("WARN: This is a warning message"); expect(stripAnsi(infoLine)).toBe("INFO: This is an info message"); - LogsDX.resetInstance(); const dracula = LogsDX.getInstance({ theme: "dracula", - outputFormat: "ansi", + outputFormat: "ansi", }); const draculaError = dracula.processLine("ERROR: This is an error message"); - expect(stripAnsi(draculaError)).toBe("ERROR: This is an error message"); }); - - test("applies style codes like bold and italic", () => { - const instance = LogsDX.getInstance({ theme: { name: "test-theme", @@ -263,14 +246,11 @@ describe("ANSI theming integration", () => { outputFormat: "ansi", }); - const errorLine = instance.processLine("ERROR: Critical failure"); - const plainText = stripAnsi(errorLine); expect(plainText).toBe("ERROR: Critical failure"); - expect(instance.getCurrentOutputFormat()).toBe("ansi"); }); }); diff --git a/tests/unit/renderer/constants.test.ts b/tests/unit/renderer/constants.test.ts index 51f7e56..869ab85 100644 --- a/tests/unit/renderer/constants.test.ts +++ b/tests/unit/renderer/constants.test.ts @@ -45,7 +45,6 @@ describe("Color Support Detection", () => { (process.stdout as NodeJS.WriteStream & { isTTY?: boolean }).isTTY = true; delete process.env.TERM; - const expectedResult = typeof Bun !== "undefined" ? true : false; expect(supportsColors()).toBe(expectedResult); }); diff --git a/tests/unit/renderer/index.test.ts b/tests/unit/renderer/index.test.ts index e19cea1..a09083e 100644 --- a/tests/unit/renderer/index.test.ts +++ b/tests/unit/renderer/index.test.ts @@ -17,7 +17,7 @@ describe("Renderer", () => { describe("renderLine", () => { test("renders a simple line with default options", () => { const result = renderLine("test line"); - + expect(result).toContain("t"); expect(result).toContain("e"); expect(result).toContain("s"); @@ -29,7 +29,7 @@ describe("Renderer", () => { outputFormat: "html", htmlStyleFormat: "css", }); - + expect(result).toContain(""); @@ -40,7 +40,7 @@ describe("Renderer", () => { outputFormat: "html", htmlStyleFormat: "className", }); - + expect(result).toContain(""); @@ -87,9 +87,9 @@ describe("Renderer", () => { }, }, ]; - const result = tokensToString(tokens, true); + const result = tokensToString(tokens, true); expect(result).toContain("error"); - expect(result).toContain("\x1b[31m"); + expect(result).toContain("\x1b[31m"); }); test("applies multiple style codes to tokens", () => { @@ -104,10 +104,10 @@ describe("Renderer", () => { }, }, ]; - const result = tokensToString(tokens, true); + const result = tokensToString(tokens, true); expect(result).toContain("important"); - expect(result).toContain("\x1b[1m"); - expect(result).toContain("\x1b[4m"); + expect(result).toContain("\x1b[1m"); + expect(result).toContain("\x1b[4m"); }); }); @@ -193,7 +193,7 @@ describe("Renderer", () => { test("applyColor adds ANSI color codes", () => { const result = applyColor("text", "red"); expect(result).toContain("text"); - + expect(typeof result).toBe("string"); expect(result).toMatch(/text/); }); @@ -221,9 +221,71 @@ describe("Renderer", () => { test("applyBackgroundColor adds ANSI background color", () => { const result = applyBackgroundColor("text", "blue"); expect(result).toContain("text"); - + expect(typeof result).toBe("string"); expect(result).toMatch(/text/); }); }); + + + describe("edge cases", () => { + test("handles tokens with trimmed metadata", () => { + const tokens: TokenList = [ + { + content: " ", + metadata: { + matchType: "spaces", + trimmed: true, + originalLength: 2, + }, + }, + ]; + const result = tokensToString(tokens); + expect(result).toBe(" "); + }); + + test("handles tokens with all style codes", () => { + const tokens: TokenList = [ + { + content: "text", + metadata: { + style: { + color: "#ff0000", + styleCodes: ["bold", "italic", "underline", "dim"], + }, + }, + }, + ]; + const resultAnsi = tokensToString(tokens, true); + expect(resultAnsi).toContain("text"); + + const resultHtml = tokensToHtml(tokens); + expect(resultHtml).toContain("text"); + expect(resultHtml).toContain("font-weight: bold"); + expect(resultHtml).toContain("font-style: italic"); + expect(resultHtml).toContain("text-decoration: underline"); + }); + + test("handles carriage return in HTML", () => { + const tokens: TokenList = [ + { + content: "\r", + metadata: { matchType: "carriage-return" }, + }, + ]; + const result = tokensToHtml(tokens); + expect(result).toBe(""); + }); + + test("handles tab tokens in HTML", () => { + const tokens: TokenList = [ + { + content: "\t", + metadata: { matchType: "tab" }, + }, + ]; + const result = tokensToHtml(tokens); + expect(result).toContain(" "); + }); + }); }); diff --git a/tests/unit/schema/validator.test.ts b/tests/unit/schema/validator.test.ts index 17dfaca..f00f200 100644 --- a/tests/unit/schema/validator.test.ts +++ b/tests/unit/schema/validator.test.ts @@ -125,7 +125,6 @@ describe("Schema Validator", () => { test("throws on invalid theme", () => { const invalidTheme = { name: "Dark Theme", - }; expect(() => validateTheme(invalidTheme)).toThrow(); @@ -153,7 +152,6 @@ describe("Schema Validator", () => { test("returns error for invalid theme", () => { const invalidTheme = { name: "Dark Theme", - }; const result = validateThemeSafe(invalidTheme); @@ -167,11 +165,9 @@ describe("Schema Validator", () => { const jsonSchema = convertTokenSchemaToJson(); expect(jsonSchema).toHaveProperty("$schema"); - + expect(typeof jsonSchema).toBe("object"); - - const hasNameReference = JSON.stringify(jsonSchema).includes("Token"); expect(hasNameReference).toBe(true); }); @@ -182,11 +178,9 @@ describe("Schema Validator", () => { const jsonSchema = convertThemeSchemaToJson(); expect(jsonSchema).toHaveProperty("$schema"); - + expect(typeof jsonSchema).toBe("object"); - - const hasNameReference = JSON.stringify(jsonSchema).includes("Theme"); expect(hasNameReference).toBe(true); }); diff --git a/tests/unit/themes/index.test.ts b/tests/unit/themes/index.test.ts index 126e8c1..727b553 100644 --- a/tests/unit/themes/index.test.ts +++ b/tests/unit/themes/index.test.ts @@ -8,11 +8,9 @@ import { import { THEMES, DEFAULT_THEME } from "../../../src/themes/constants"; describe("Theme Management", () => { - const originalConsoleLog = console.log; let consoleOutput: string[] = []; - beforeEach(() => { consoleOutput = []; console.log = (message: string) => { @@ -20,7 +18,6 @@ describe("Theme Management", () => { }; }); - afterEach(() => { console.log = originalConsoleLog; }); @@ -72,7 +69,6 @@ describe("Theme Management", () => { registerTheme(testTheme); - expect(getTheme("test-theme")).toEqual(testTheme); expect(getThemeNames()).toContain("test-theme"); expect(getAllThemes()["test-theme"]).toEqual(testTheme); @@ -106,7 +102,6 @@ describe("Theme Management", () => { registerTheme(firstTheme); registerTheme(secondTheme); - expect(getTheme("overwrite-test")).toEqual(secondTheme); expect(getTheme("overwrite-test").description).toBe("Second version"); }); diff --git a/tests/unit/themes/template.test.ts b/tests/unit/themes/template.test.ts index 9d9e94a..ed779e2 100644 --- a/tests/unit/themes/template.test.ts +++ b/tests/unit/themes/template.test.ts @@ -239,7 +239,7 @@ describe("Theme Generation", () => { const theme = generateTemplate(config); expect(theme.schema.matchWords?.CUSTOM).toBeDefined(); - expect(theme.schema.matchWords?.CUSTOM.color).toBe("#d1242f"); + expect(theme.schema.matchWords?.CUSTOM.color).toBe("#d1242f"); expect(theme.schema.matchWords?.CUSTOM.styleCodes).toEqual(["bold"]); }); @@ -252,11 +252,9 @@ describe("Theme Generation", () => { const theme = generateTemplate(config); - - expect(theme.schema.matchWords?.INFO).toBeDefined(); - expect(theme.schema.matchWords?.GET).toBeDefined(); + expect(theme.schema.matchWords?.INFO).toBeDefined(); + expect(theme.schema.matchWords?.GET).toBeDefined(); - const timestampPattern = theme.schema.matchPatterns?.find( (p) => p.name === "timestamp-iso", ); diff --git a/tests/unit/themes/utils.test.ts b/tests/unit/themes/utils.test.ts index 509f51b..d69c8ff 100644 --- a/tests/unit/themes/utils.test.ts +++ b/tests/unit/themes/utils.test.ts @@ -38,8 +38,8 @@ describe("themes/utils", () => { }); test("correctly identifies medium luminance colors", () => { - expect(isDarkColor("#808080")).toBe(false); - expect(isDarkColor("#404040")).toBe(true); + expect(isDarkColor("#808080")).toBe(false); + expect(isDarkColor("#404040")).toBe(true); }); }); @@ -102,7 +102,6 @@ describe("themes/utils", () => { const colorsAA = getAccessibleTextColors("#000000", "AA"); const colorsAAA = getAccessibleTextColors("#000000", "AAA"); - expect(colorsAAA.text).not.toBe(colorsAA.text); }); @@ -110,7 +109,6 @@ describe("themes/utils", () => { const colorsAA = getAccessibleTextColors("#FFFFFF", "AA"); const colorsAAA = getAccessibleTextColors("#FFFFFF", "AAA"); - expect(colorsAAA.text).not.toBe(colorsAA.text); }); }); diff --git a/tests/unit/tokenizer/index.test.ts b/tests/unit/tokenizer/index.test.ts index bafab6d..f1c3ea0 100644 --- a/tests/unit/tokenizer/index.test.ts +++ b/tests/unit/tokenizer/index.test.ts @@ -36,7 +36,6 @@ describe("Tokenizer", () => { expect(tokens).toBeInstanceOf(Array); expect(tokens.length).toBeGreaterThan(0); - const totalContent = tokens.map((t) => t.content).join(""); expect(totalContent).toBe(line); }); @@ -45,15 +44,11 @@ describe("Tokenizer", () => { const line = "2023-01-01T12:00:00Z ERROR: Something went wrong"; const tokens = tokenize(line); - - const content = tokens.map((t) => t.content).join(""); expect(content).toBe(line); - expect(tokens.length).toBeGreaterThan(1); - const hasError = tokens.some((t) => t.content.includes("ERROR")); expect(hasError).toBe(true); }); @@ -61,12 +56,10 @@ describe("Tokenizer", () => { test("handles whitespace according to theme preferences", () => { const line = "test with spaces"; - const tokensPreserve = tokenize(line); const contentPreserve = tokensPreserve.map((t) => t.content).join(""); expect(contentPreserve).toBe(line); - const themeTrim: Theme = { name: "Trim Whitespace", schema: { @@ -74,7 +67,6 @@ describe("Tokenizer", () => { }, }; - const tokensTrim = tokenize(line, themeTrim); expect(tokensTrim).toBeInstanceOf(Array); expect(tokensTrim.length).toBeGreaterThan(0); @@ -83,12 +75,10 @@ describe("Tokenizer", () => { test("handles newlines according to theme preferences", () => { const line = "line1\nline2"; - const tokensPreserve = tokenize(line); const hasNewline = tokensPreserve.some((t) => t.content === "\n"); expect(hasNewline).toBe(true); - const themeTrim: Theme = { name: "Trim Newlines", schema: { @@ -96,14 +86,12 @@ describe("Tokenizer", () => { }, }; - const tokensTrim = tokenize(line, themeTrim); expect(tokensTrim).toBeInstanceOf(Array); expect(tokensTrim.length).toBeGreaterThan(0); }); test("applies theme-specific word matching", () => { - const theme: Theme = { name: "Word Theme", schema: { @@ -113,7 +101,6 @@ describe("Tokenizer", () => { }, }; - const tokens: TokenList = [ { content: "test", @@ -124,15 +111,12 @@ describe("Tokenizer", () => { }, ]; - const styledTokens = applyTheme(tokens, theme); - expect(styledTokens[0].metadata?.style?.color).toBe("green"); }); test("applies theme-specific pattern matching", () => { - const tokens: TokenList = [ { content: "123", @@ -144,7 +128,6 @@ describe("Tokenizer", () => { }, ]; - const theme: Theme = { name: "Simple Pattern Theme", schema: { @@ -158,18 +141,14 @@ describe("Tokenizer", () => { }, }; - const styledTokens = applyTheme(tokens, theme); - expect(styledTokens[0].metadata?.style?.color).toBe("blue"); }); test("handles invalid regex patterns gracefully", () => { - const originalWarn = console.warn; - console.warn = () => {}; try { diff --git a/tests/unit/utils/logger.test.ts b/tests/unit/utils/logger.test.ts index e1eb49c..06e8492 100644 --- a/tests/unit/utils/logger.test.ts +++ b/tests/unit/utils/logger.test.ts @@ -2,9 +2,7 @@ import { describe, expect, test, spyOn, afterEach } from "bun:test"; import { logger } from "../../../src/utils/logger"; describe("logger", () => { - afterEach(() => { - - }); + afterEach(() => {}); test("info() logs with blue info icon", () => { const spy = spyOn(console, "log"); diff --git a/tests/unit/utils/spinner.test.ts b/tests/unit/utils/spinner.test.ts index bf60985..de50286 100644 --- a/tests/unit/utils/spinner.test.ts +++ b/tests/unit/utils/spinner.test.ts @@ -94,8 +94,8 @@ describe("spinner", () => { s.stop(); const output = mockWrites.join(""); - expect(output).toContain("\x1B[K"); - expect(output).toContain("\x1B[?25h"); + expect(output).toContain("\x1B[K"); + expect(output).toContain("\x1B[?25h"); }); test("start should hide cursor and write frames", async () => { @@ -106,7 +106,7 @@ describe("spinner", () => { s.stop(); const output = mockWrites.join(""); - expect(output).toContain("\x1B[?25l"); + expect(output).toContain("\x1B[?25l"); expect(output).toContain("Test"); }); From 0a581bf8d64b46ce551be4cdc4740a202ad2ff6c Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:19:27 -0800 Subject: [PATCH 3/6] chore: adds more tests --- tests/unit/renderer/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/renderer/index.test.ts b/tests/unit/renderer/index.test.ts index a09083e..7ff8d28 100644 --- a/tests/unit/renderer/index.test.ts +++ b/tests/unit/renderer/index.test.ts @@ -227,7 +227,6 @@ describe("Renderer", () => { }); }); - describe("edge cases", () => { test("handles tokens with trimmed metadata", () => { const tokens: TokenList = [ From c357b116911769e99730480665013a19eb11caf3 Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Tue, 11 Nov 2025 00:11:35 -0800 Subject: [PATCH 4/6] chore: adds more tests --- tests/unit/cli/theme/generator.test.ts | 194 ++++++++++++++++++++++- tests/unit/cli/theme/transporter.test.ts | 83 +++++++++- tests/unit/renderer/index.test.ts | 117 +++++++++++++- tests/unit/schema/validator.test.ts | 45 ++++++ tests/unit/tokenizer/index.test.ts | 67 ++++++++ tests/unit/utils/prompts.test.ts | 159 +++++++++++++++++++ 6 files changed, 662 insertions(+), 3 deletions(-) create mode 100644 tests/unit/utils/prompts.test.ts diff --git a/tests/unit/cli/theme/generator.test.ts b/tests/unit/cli/theme/generator.test.ts index cb85d1f..04d9004 100644 --- a/tests/unit/cli/theme/generator.test.ts +++ b/tests/unit/cli/theme/generator.test.ts @@ -1,14 +1,29 @@ -import { describe, it, expect } from "bun:test"; +import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; import { generateTemplateFromAnswers, validateColorInput, generatePatternFromPreset, + listColorPalettesCommand, + listPatternPresetsCommand, } from "../../../../src/cli/theme/generator"; import { COLOR_PALETTES, PATTERN_PRESETS, } from "../../../../src/themes/template"; +const originalLog = console.log; +const originalWarn = console.warn; + +beforeEach(() => { + console.log = mock(() => {}); + console.warn = mock(() => {}); +}); + +afterEach(() => { + console.log = originalLog; + console.warn = originalWarn; +}); + describe("Theme Generator", () => { describe("validateColorInput", () => { it("should validate hex colors", () => { @@ -30,6 +45,28 @@ describe("Theme Generator", () => { expect(validateColorInput("blue")).toBe(true); expect(validateColorInput("notacolor")).toBe(true); }); + + it("should reject empty or invalid strings", () => { + expect(validateColorInput("")).toBe(false); + expect(validateColorInput(" ")).toBe(false); + expect(validateColorInput(null as any)).toBe(false); + expect(validateColorInput(undefined as any)).toBe(false); + }); + + it("should reject hex without hash", () => { + expect(validateColorInput("aabbcc")).toBe(false); + expect(validateColorInput("123456")).toBe(false); + }); + + it("should validate various named colors", () => { + expect(validateColorInput("green")).toBe(true); + expect(validateColorInput("yellow")).toBe(true); + expect(validateColorInput("cyan")).toBe(true); + expect(validateColorInput("magenta")).toBe(true); + expect(validateColorInput("white")).toBe(true); + expect(validateColorInput("black")).toBe(true); + expect(validateColorInput("gray")).toBe(true); + }); }); describe("generatePatternFromPreset", () => { @@ -56,6 +93,20 @@ describe("Theme Generator", () => { }); }); + it("should generate UUID pattern", () => { + const pattern = generatePatternFromPreset("uuid"); + expect(pattern).toHaveProperty("name", "uuid"); + expect(pattern).toHaveProperty("pattern"); + expect(pattern).toHaveProperty("options"); + }); + + it("should generate URL pattern", () => { + const pattern = generatePatternFromPreset("url"); + expect(pattern).toHaveProperty("name", "url"); + expect(pattern).toHaveProperty("pattern"); + expect(pattern).toHaveProperty("options"); + }); + it("should return empty object for unknown preset", () => { const pattern = generatePatternFromPreset("unknown"); expect(pattern).toEqual({}); @@ -126,6 +177,102 @@ describe("Theme Generator", () => { expect(theme.schema.matchStartsWith?.["["]).toBeDefined(); expect(theme.schema.matchEndsWith?.["]"]).toBeDefined(); }); + + it("should handle httpStatus feature", () => { + const theme = generateTemplateFromAnswers({ + name: "http-theme", + mode: "dark", + palette: "github-dark", + features: ["httpStatus"], + patterns: [], + }); + + expect(theme.schema.matchWords).toHaveProperty("200"); + expect(theme.schema.matchWords).toHaveProperty("404"); + expect(theme.schema.matchWords).toHaveProperty("500"); + }); + + it("should handle custom patterns in answers", () => { + const theme = generateTemplateFromAnswers({ + name: "custom-pattern-theme", + mode: "dark", + palette: "github-dark", + features: [], + patterns: [], + customPatterns: [ + { + name: "test-pattern", + pattern: "\\d+", + color: "#ff0000", + styleCodes: ["bold"], + }, + ], + }); + + expect(theme.schema).toBeDefined(); + }); + + it("should handle custom words in answers", () => { + const theme = generateTemplateFromAnswers({ + name: "custom-words-theme", + mode: "dark", + palette: "github-dark", + features: [], + patterns: [], + customWords: { + CUSTOM: { colorRole: "error", styleCodes: ["bold"] }, + }, + }); + + expect(theme.schema).toBeDefined(); + }); + + it("should use themeName field when name is not provided", () => { + const theme = generateTemplateFromAnswers({ + themeName: "fallback-name", + mode: "dark", + palette: "github-dark", + features: [], + patterns: [], + }); + + expect(theme.name).toBe("fallback-name"); + }); + + it("should use patternPresets field when patterns is not provided", () => { + const theme = generateTemplateFromAnswers({ + name: "preset-theme", + mode: "dark", + palette: "github-dark", + features: [], + patternPresets: ["log-levels"], + }); + + expect(theme.schema.matchPatterns).toBeDefined(); + }); + + it("should handle empty mode", () => { + const theme = generateTemplateFromAnswers({ + name: "no-mode", + palette: "github-dark", + features: [], + patterns: [], + }); + + expect(theme.name).toBe("no-mode"); + }); + + it("should add log-levels to patterns when logLevels feature is present", () => { + const theme = generateTemplateFromAnswers({ + name: "log-levels-feature", + mode: "dark", + palette: "github-dark", + features: ["logLevels"], + patterns: [], + }); + + expect(theme.schema.matchWords).toHaveProperty("ERROR"); + }); }); describe("COLOR_PALETTES", () => { @@ -168,4 +315,49 @@ describe("Theme Generator", () => { }); }); }); + + describe("listColorPalettesCommand", () => { + it("should display all color palettes", () => { + listColorPalettesCommand(); + + expect(console.log).toHaveBeenCalled(); + }); + + it("should display palette details", () => { + listColorPalettesCommand(); + + const calls = (console.log as ReturnType).mock.calls; + const allOutput = calls.map((call) => call.join(" ")).join("\n"); + + expect(allOutput).toContain("github-dark"); + expect(allOutput).toContain("github-light"); + }); + }); + + describe("listPatternPresetsCommand", () => { + it("should display all pattern presets", () => { + listPatternPresetsCommand(); + + expect(console.log).toHaveBeenCalled(); + }); + + it("should display presets grouped by category", () => { + listPatternPresetsCommand(); + + const calls = (console.log as ReturnType).mock.calls; + const allOutput = calls.map((call) => call.join(" ")).join("\n"); + + expect(allOutput).toContain("log-levels"); + }); + + it("should show pattern and word match counts", () => { + listPatternPresetsCommand(); + + const calls = (console.log as ReturnType).mock.calls; + const allOutput = calls.map((call) => call.join(" ")).join("\n"); + + expect(allOutput).toContain("patterns"); + expect(allOutput).toContain("word matches"); + }); + }); }); diff --git a/tests/unit/cli/theme/transporter.test.ts b/tests/unit/cli/theme/transporter.test.ts index 30a3a02..925b683 100644 --- a/tests/unit/cli/theme/transporter.test.ts +++ b/tests/unit/cli/theme/transporter.test.ts @@ -1,13 +1,17 @@ -import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "fs"; import { join } from "path"; import { exportThemeToFile, importThemeFromFile, listThemeFiles, + getThemeFiles, + listThemeFilesCommand, } from "../../../../src/cli/theme/transporter"; import type { Theme } from "../../../../src/types"; +const originalLog = console.log; + const TEST_DIR = join(process.cwd(), ".test-themes"); describe("Theme Transporter", () => { @@ -15,12 +19,14 @@ describe("Theme Transporter", () => { if (!existsSync(TEST_DIR)) { mkdirSync(TEST_DIR, { recursive: true }); } + console.log = mock(() => {}); }); afterEach(() => { if (existsSync(TEST_DIR)) { rmSync(TEST_DIR, { recursive: true, force: true }); } + console.log = originalLog; }); const sampleTheme: Theme = { @@ -212,4 +218,79 @@ describe("Theme Transporter", () => { ); }); }); + + describe("getThemeFiles", () => { + it("should be the same as listThemeFiles", () => { + expect(getThemeFiles).toBe(listThemeFiles); + }); + }); + + describe("listThemeFilesCommand", () => { + it("should display no theme files message when directory is empty", () => { + listThemeFilesCommand(TEST_DIR); + + expect(console.log).toHaveBeenCalled(); + }); + + it("should list theme files with metadata", () => { + const themeFile = join(TEST_DIR, "test.theme.json"); + writeFileSync(themeFile, JSON.stringify(sampleTheme, null, 2)); + + listThemeFilesCommand(TEST_DIR); + + expect(console.log).toHaveBeenCalled(); + const calls = (console.log as ReturnType).mock.calls; + const allOutput = calls.map((call) => call.join(" ")).join("\n"); + + expect(allOutput).toContain("test-theme"); + expect(allOutput).toContain("A test theme"); + }); + + it("should handle invalid JSON theme files", () => { + const invalidFile = join(TEST_DIR, "invalid.theme.json"); + writeFileSync(invalidFile, "{ invalid json }"); + + listThemeFilesCommand(TEST_DIR); + + expect(console.log).toHaveBeenCalled(); + const calls = (console.log as ReturnType).mock.calls; + const allOutput = calls.map((call) => call.join(" ")).join("\n"); + + expect(allOutput).toContain("Error"); + }); + + it("should display exportedAt timestamp if present", () => { + const themeWithExport = { + ...sampleTheme, + exportedAt: new Date().toISOString(), + }; + const themeFile = join(TEST_DIR, "exported.theme.json"); + writeFileSync(themeFile, JSON.stringify(themeWithExport, null, 2)); + + listThemeFilesCommand(TEST_DIR); + + expect(console.log).toHaveBeenCalled(); + const calls = (console.log as ReturnType).mock.calls; + const allOutput = calls.map((call) => call.join(" ")).join("\n"); + + expect(allOutput).toContain("Exported"); + }); + + it("should handle themes without description", () => { + const themeWithoutDesc = { + name: "minimal-theme", + schema: {}, + }; + const themeFile = join(TEST_DIR, "minimal.theme.json"); + writeFileSync(themeFile, JSON.stringify(themeWithoutDesc, null, 2)); + + listThemeFilesCommand(TEST_DIR); + + expect(console.log).toHaveBeenCalled(); + const calls = (console.log as ReturnType).mock.calls; + const allOutput = calls.map((call) => call.join(" ")).join("\n"); + + expect(allOutput).toContain("minimal-theme"); + }); + }); }); diff --git a/tests/unit/renderer/index.test.ts b/tests/unit/renderer/index.test.ts index 7ff8d28..8c338b0 100644 --- a/tests/unit/renderer/index.test.ts +++ b/tests/unit/renderer/index.test.ts @@ -228,7 +228,21 @@ describe("Renderer", () => { }); describe("edge cases", () => { - test("handles tokens with trimmed metadata", () => { + test("handles tokens with trimmed spaces without originalLength", () => { + const tokens: TokenList = [ + { + content: " ", + metadata: { + matchType: "spaces", + trimmed: true, + }, + }, + ]; + const result = tokensToString(tokens); + expect(result).toBe(""); + }); + + test("handles tokens with trimmed metadata and originalLength", () => { const tokens: TokenList = [ { content: " ", @@ -286,5 +300,106 @@ describe("Renderer", () => { const result = tokensToHtml(tokens); expect(result).toContain(" "); }); + + test("handles background color in styles", () => { + const tokens: TokenList = [ + { + content: "text", + metadata: { + style: { + color: "#ff0000", + backgroundColor: "#000000", + }, + }, + }, + ]; + const result = tokensToString(tokens, true); + expect(result).toContain("text"); + }); + + test("handles tokens without style metadata", () => { + const tokens: TokenList = [ + { + content: "plain text", + metadata: {}, + }, + ]; + const result = tokensToString(tokens, true); + expect(result).toBe("plain text"); + }); + + test("handles single space match type", () => { + const tokens: TokenList = [ + { + content: " ", + metadata: { + matchType: "space", + }, + }, + ]; + const result = tokensToString(tokens); + expect(result).toBe(" "); + }); + + test("handles mixed token types", () => { + const tokens: TokenList = [ + { + content: "text", + metadata: { matchType: "word" }, + }, + { + content: " ", + metadata: { matchType: "space" }, + }, + { + content: "more", + metadata: { matchType: "word" }, + }, + ]; + const result = tokensToString(tokens); + expect(result).toBe("text more"); + }); + + test("handles trimmed spaces in HTML", () => { + const tokens: TokenList = [ + { + content: " ", + metadata: { + matchType: "spaces", + trimmed: true, + }, + }, + ]; + const result = tokensToHtml(tokens); + expect(result).toBe(" "); + }); + + test("handles trimmed token with TRIMMED_SPACE_MATCH_TYPES in HTML", () => { + const tokens: TokenList = [ + { + content: " ", + metadata: { + matchType: "spaces", + trimmed: true, + originalLength: 2, + }, + }, + ]; + const result = tokensToHtml(tokens); + expect(result).toBe(" "); + }); + + test("handles spaces match type in HTML", () => { + const tokens: TokenList = [ + { + content: " ", + metadata: { + matchType: "spaces", + }, + }, + ]; + const result = tokensToHtml(tokens); + expect(result).toContain(" "); + }); }); }); diff --git a/tests/unit/schema/validator.test.ts b/tests/unit/schema/validator.test.ts index f00f200..1d32bd5 100644 --- a/tests/unit/schema/validator.test.ts +++ b/tests/unit/schema/validator.test.ts @@ -8,6 +8,7 @@ import { validateThemeSafe, convertTokenSchemaToJson, convertThemeSchemaToJson, + createThemeValidationError, } from "../../../src/schema/validator"; import { z } from "zod"; @@ -185,4 +186,48 @@ describe("Schema Validator", () => { expect(hasNameReference).toBe(true); }); }); + + describe("createThemeValidationError", () => { + test("returns Error object as is when given Error", () => { + const originalError = new Error("Custom error message"); + + const result = createThemeValidationError(originalError); + + expect(result).toBe(originalError); + expect(result.message).toBe("Custom error message"); + }); + + test("converts string to Error when given string", () => { + const errorString = "Something went wrong"; + + const result = createThemeValidationError(errorString); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe("Something went wrong"); + }); + + test("converts number to Error when given number", () => { + const errorNumber = 404; + + const result = createThemeValidationError(errorNumber); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe("404"); + }); + + test("formats ZodError with validation message", () => { + const invalidTheme = { + name: "test", + }; + + try { + validateTheme(invalidTheme); + } catch (error) { + const result = createThemeValidationError(error); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Theme validation failed"); + } + }); + }); }); diff --git a/tests/unit/tokenizer/index.test.ts b/tests/unit/tokenizer/index.test.ts index f1c3ea0..5b43554 100644 --- a/tests/unit/tokenizer/index.test.ts +++ b/tests/unit/tokenizer/index.test.ts @@ -392,6 +392,73 @@ describe("Tokenizer", () => { const content = tokens.map((t) => t.content).join(""); expect(content).toBe(line); }); + + test("trims multiple spaces when whiteSpace is trim", () => { + const theme: Theme = { + name: "Trim Spaces", + schema: { + whiteSpace: "trim", + }, + }; + const line = "text with multiple spaces"; + const tokens = tokenize(line, theme); + + expect(tokens).toBeInstanceOf(Array); + expect(tokens.length).toBeGreaterThan(0); + + const hasSpaceTokens = tokens.some( + (t) => + t.metadata?.matchType === "spaces" || + t.metadata?.matchType === "space", + ); + expect(hasSpaceTokens).toBe(true); + }); + + test("trims single space when whiteSpace is trim", () => { + const theme: Theme = { + name: "Trim Space", + schema: { + whiteSpace: "trim", + }, + }; + const line = "text with space"; + const tokens = tokenize(line, theme); + + const spaceTokens = tokens.filter( + (t) => + t.metadata?.matchType === "space" || + t.metadata?.matchType === "spaces", + ); + expect(spaceTokens.length).toBeGreaterThan(0); + }); + + test("handles newlines when newLine is trim", () => { + const theme: Theme = { + name: "Trim Newlines", + schema: { + newLine: "trim", + }, + }; + const line = "line1\nline2\nline3"; + const tokens = tokenize(line, theme); + + expect(tokens).toBeInstanceOf(Array); + expect(tokens.length).toBeGreaterThan(0); + }); + + test("handles carriage returns when newLine is trim", () => { + const theme: Theme = { + name: "Trim CR", + schema: { + newLine: "trim", + }, + }; + const line = "line1\r\nline2"; + const tokens = tokenize(line, theme); + + expect(tokens).toBeInstanceOf(Array); + expect(tokens.length).toBeGreaterThan(0); + }); }); describe("pattern matching with identifiers", () => { diff --git a/tests/unit/utils/prompts.test.ts b/tests/unit/utils/prompts.test.ts new file mode 100644 index 0000000..1c67e39 --- /dev/null +++ b/tests/unit/utils/prompts.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; +import { EventEmitter } from "events"; + +class MockReadline extends EventEmitter { + private responses: string[] = []; + private responseIndex = 0; + + setResponses(responses: string[]) { + this.responses = responses; + this.responseIndex = 0; + } + + question(prompt: string, callback: (answer: string) => void) { + const response = this.responses[this.responseIndex++] || ""; + setImmediate(() => callback(response)); + } + + close() {} +} + +const mockRl = new MockReadline(); + +const originalCreateInterface = require("readline").createInterface; + +beforeEach(() => { + require("readline").createInterface = mock(() => mockRl); +}); + +afterEach(() => { + require("readline").createInterface = originalCreateInterface; + mockRl.removeAllListeners(); +}); + +describe("prompts (with mocks)", () => { + describe("input", () => { + it("should return user input", async () => { + mockRl.setResponses(["test value"]); + + const { input } = await import("../../../src/utils/prompts"); + const result = await input({ message: "Enter value" }); + + expect(result).toBe("test value"); + }); + + it("should return default value when empty", async () => { + mockRl.setResponses([""]); + + const { input } = await import("../../../src/utils/prompts"); + const result = await input({ + message: "Enter value", + default: "default", + }); + + expect(result).toBe("default"); + }); + + it("should validate input", async () => { + mockRl.setResponses(["invalid", "valid"]); + + const { input } = await import("../../../src/utils/prompts"); + const validate = (val: string) => val === "valid" || "Must be valid"; + const result = await input({ + message: "Enter value", + validate, + }); + + expect(result).toBe("valid"); + }); + }); + + describe("select", () => { + it("should return selected choice", async () => { + mockRl.setResponses(["2"]); + + const originalLog = console.log; + console.log = mock(() => {}); + + const { select } = await import("../../../src/utils/prompts"); + const result = await select({ + message: "Choose", + choices: ["a", "b", "c"], + }); + + console.log = originalLog; + expect(result).toBe("b"); + }); + + it("should use default when empty input", async () => { + mockRl.setResponses([""]); + + const originalLog = console.log; + console.log = mock(() => {}); + + const { select } = await import("../../../src/utils/prompts"); + const result = await select({ + message: "Choose", + choices: ["a", "b", "c"], + default: "b", + }); + + console.log = originalLog; + expect(result).toBe("b"); + }); + }); + + describe("checkbox", () => { + it("should return selected values", async () => { + mockRl.setResponses(["1,3"]); + + const originalLog = console.log; + console.log = mock(() => {}); + + const { checkbox } = await import("../../../src/utils/prompts"); + const result = await checkbox({ + message: "Select", + choices: [ + { name: "A", value: "a" }, + { name: "B", value: "b" }, + { name: "C", value: "c" }, + ], + }); + + console.log = originalLog; + expect(result).toEqual(["a", "c"]); + }); + }); + + describe("confirm", () => { + it("should return true for yes", async () => { + mockRl.setResponses(["y"]); + + const { confirm } = await import("../../../src/utils/prompts"); + const result = await confirm({ message: "Confirm?" }); + + expect(result).toBe(true); + }); + + it("should return false for no", async () => { + mockRl.setResponses(["n"]); + + const { confirm } = await import("../../../src/utils/prompts"); + const result = await confirm({ message: "Confirm?" }); + + expect(result).toBe(false); + }); + + it("should use default when empty", async () => { + mockRl.setResponses([""]); + + const { confirm } = await import("../../../src/utils/prompts"); + const result = await confirm({ + message: "Confirm?", + default: true, + }); + + expect(result).toBe(true); + }); + }); +}); From 1a4934add9ed55e7eea6b08d0e06b96763b7e14a Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:24:26 -0800 Subject: [PATCH 5/6] chore: updates dir structure --- scripts/install-hooks.ts | 6 +- src/cli/bin.ts | 2 +- src/cli/{commands/theme.ts => commands.ts} | 20 +- src/cli/index.ts | 5 +- src/{utils/cli/index.ts => cli/parser.ts} | 69 +++- src/cli/{theme/generator.ts => theme-gen.ts} | 359 +++++++++++++++++- src/cli/theme/transporter.ts | 354 ----------------- src/themes/builder.ts | 4 +- src/themes/{color-constants.ts => colors.ts} | 0 src/themes/{template.ts => presets.ts} | 0 src/themes/utils.ts | 2 +- src/utils/cli/constants.ts | 4 - src/utils/cli/types.ts | 15 - src/utils/cli/utils.ts | 29 -- src/utils/formatting.ts | 192 ++++++++++ src/utils/prompts.ts | 1 - src/utils/terminal.ts | 154 ++++++++ tests/unit/cli/theme/generator.test.ts | 4 +- tests/unit/cli/theme/transporter.test.ts | 2 +- .../{template.test.ts => presets.test.ts} | 2 +- 20 files changed, 771 insertions(+), 453 deletions(-) rename src/cli/{commands/theme.ts => commands.ts} (95%) rename src/{utils/cli/index.ts => cli/parser.ts} (75%) rename src/cli/{theme/generator.ts => theme-gen.ts} (59%) delete mode 100644 src/cli/theme/transporter.ts rename src/themes/{color-constants.ts => colors.ts} (100%) rename src/themes/{template.ts => presets.ts} (100%) delete mode 100644 src/utils/cli/constants.ts delete mode 100644 src/utils/cli/types.ts delete mode 100644 src/utils/cli/utils.ts create mode 100644 src/utils/formatting.ts create mode 100644 src/utils/terminal.ts rename tests/unit/themes/{template.test.ts => presets.test.ts} (99%) diff --git a/scripts/install-hooks.ts b/scripts/install-hooks.ts index 299496c..a0fa3ff 100644 --- a/scripts/install-hooks.ts +++ b/scripts/install-hooks.ts @@ -6,12 +6,12 @@ import { join } from "path"; const HOOKS_DIR = ".git/hooks"; const PRE_COMMIT = `#!/bin/sh -cd "\$(git rev-parse --show-toplevel)" || exit 1 +cd "$(git rev-parse --show-toplevel)" || exit 1 bun run format && bun run lint && bun test `; const POST_CHECKOUT = `#!/bin/sh -changed=\$(git diff-tree -r --name-only --no-commit-id $1 $2 2>/dev/null) +changed=$(git diff-tree -r --name-only --no-commit-id $1 $2 2>/dev/null) if echo "$changed" | grep -q "package.json\\|bun.lock"; then echo "Dependencies changed, running bun install..." bun install @@ -19,7 +19,7 @@ fi `; const POST_MERGE = `#!/bin/sh -changed=\$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD 2>/dev/null) +changed=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD 2>/dev/null) if echo "$changed" | grep -q "package.json\\|bun.lock"; then echo "Dependencies changed, running bun install..." bun install diff --git a/src/cli/bin.ts b/src/cli/bin.ts index 0fbb96c..5988340 100644 --- a/src/cli/bin.ts +++ b/src/cli/bin.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createCLI } from "../utils/cli"; +import { createCLI } from "./parser"; import { main } from "./index"; import type { CommanderOptions } from "./types"; import { version } from "../../package.json"; diff --git a/src/cli/commands/theme.ts b/src/cli/commands.ts similarity index 95% rename from src/cli/commands/theme.ts rename to src/cli/commands.ts index ad0b158..71243bd 100644 --- a/src/cli/commands/theme.ts +++ b/src/cli/commands.ts @@ -1,9 +1,9 @@ -import { createCLI, CLI } from "../../utils/cli"; -import { input, select, checkbox, confirm } from "../../utils/prompts"; -import spinner from "../../utils/spinner"; -import * as colorUtil from "../../utils/colors"; -import gradient from "../../utils/gradient"; -import boxen from "../../utils/boxen"; +import { createCLI, CLI } from "./parser"; +import { input, select, checkbox, confirm } from "../utils/prompts"; +import spinner from "../utils/spinner"; +import * as colorUtil from "../utils/colors"; +import gradient from "../utils/gradient"; +import boxen from "../utils/boxen"; import { writeFileSync, existsSync, mkdirSync } from "fs"; import { dirname } from "path"; import { @@ -11,10 +11,10 @@ import { checkWCAGCompliance, adjustThemeForAccessibility, SimpleThemeConfig, -} from "../../themes/builder"; -import { registerTheme, getTheme, getThemeNames } from "../../themes"; -import { getLogsDX } from "../../index"; -import { Theme } from "../../types"; +} from "../themes/builder"; +import { registerTheme, getTheme, getThemeNames } from "../themes"; +import { getLogsDX } from "../index"; +import { Theme } from "../types"; const SAMPLE_LOGS = [ "INFO: Server started on port 3000", diff --git a/src/cli/index.ts b/src/cli/index.ts index ee8ba74..7e6489a 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,4 +1,3 @@ -import { createCLI } from "../utils/cli"; import fs from "fs"; import path from "path"; import { LogsDX, getThemeNames } from "../index"; @@ -15,8 +14,8 @@ import { runThemeGenerator, listColorPalettesCommand, listPatternPresetsCommand, -} from "./theme/generator"; -import { exportTheme, importTheme, listThemeFiles } from "./theme/transporter"; +} from "./theme-gen"; +import { exportTheme, importTheme, listThemeFiles } from "./theme-gen"; export function loadConfig(configPath?: string): LogsDXOptions { const defaultConfig: LogsDXOptions = { diff --git a/src/utils/cli/index.ts b/src/cli/parser.ts similarity index 75% rename from src/utils/cli/index.ts rename to src/cli/parser.ts index 9a2d06a..480d0b2 100644 --- a/src/utils/cli/index.ts +++ b/src/cli/parser.ts @@ -1,16 +1,50 @@ -import type { - OptionDefinition, - ParsedOptions, - ArgumentDefinition, -} from "./types"; -import { HELP_FLAGS, VERSION_FLAGS, BOOLEAN_FLAG_PREFIX } from "./constants"; -import { - camelCase, - findOption, - extractLongFlag, - expectsValue, - hasOptionalValue, -} from "./utils"; +export interface OptionDefinition { + flags: string; + description: string; + defaultValue?: string | boolean | number; +} + +export interface ParsedOptions { + [key: string]: string | boolean | number | undefined; +} + +export interface ArgumentDefinition { + name: string; + description: string; + required: boolean; +} + +export const HELP_FLAGS = ["--help", "-h"]; +export const VERSION_FLAGS = ["--version", "-v"]; +export const BOOLEAN_FLAG_PREFIX = "no-"; + +export function camelCase(str: string): string { + return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +export function findOption( + options: OptionDefinition[], + flag: string, +): OptionDefinition | undefined { + return options.find((o) => o.flags.includes(flag)); +} + +export function extractLongFlag(flags: string): string | null { + const match = flags.match(/--([a-z-]+)/); + return match ? match[1] : null; +} + +export function expectsValue(flags: string): boolean { + return flags.includes("<"); +} + +export function hasOptionalValue(flags: string): boolean { + return flags.includes("[") && !flags.startsWith("["); +} + +export function isBooleanFlag(flags: string): boolean { + return !expectsValue(flags) && !hasOptionalValue(flags); +} export class CLI { private programName = ""; @@ -38,14 +72,14 @@ export class CLI { return this; } - option(flags: string, description: string, defaultValue?: any): this { + option(flags: string, description: string, defaultValue?: string | boolean | number): this { this.options.push({ flags, description, defaultValue }); return this; } argument(name: string, description: string): this { const required = !name.startsWith("["); - const cleanName = name.replace(/[\[\]]/g, ""); + const cleanName = name.replace(/[[\]]/g, ""); this.argumentDef = { name: cleanName, description, required }; return this; } @@ -177,9 +211,4 @@ export function createCLI(): CLI { return new CLI(); } -export { - type OptionDefinition, - type ParsedOptions, - type ArgumentDefinition, -} from "./types"; export default CLI; diff --git a/src/cli/theme/generator.ts b/src/cli/theme-gen.ts similarity index 59% rename from src/cli/theme/generator.ts rename to src/cli/theme-gen.ts index ef4a394..7d2b5fb 100644 --- a/src/cli/theme/generator.ts +++ b/src/cli/theme-gen.ts @@ -1,7 +1,8 @@ -import { select, input, checkbox, confirm } from "../../utils/prompts"; -import { ui } from "../ui"; +import { select, input, checkbox, confirm } from "../utils/prompts"; +import { ui } from "./ui"; import chalk from "chalk"; import fs from "fs"; +import path from "path"; import { listColorPalettes, listPatternPresets, @@ -9,9 +10,11 @@ import { type ColorPalette, type PatternPreset, type ThemeGeneratorConfig, -} from "../../themes/template"; -import { registerTheme } from "../../themes"; -import type { Theme, PatternMatch } from "../../types"; +} from "../themes/presets"; +import { registerTheme, getAllThemes, getTheme } from "../themes"; +import type { Theme, PatternMatch } from "../types"; +import { themePresetSchema } from "../schema"; +import { LogsDX } from "../index"; export async function runThemeGenerator(): Promise { ui.showHeader(); @@ -284,7 +287,6 @@ async function showThemePreview(theme: Theme, palette: ColorPalette) { "192.168.1.100 - Processing request in 15ms", ]; - const { LogsDX } = await import("../../index"); registerTheme(theme); const logsDX = LogsDX.getInstance({ theme: theme.name, @@ -544,3 +546,348 @@ export function generatePatternFromPreset( return patternMap[presetName] || {}; } + +export async function exportTheme(themeName?: string): Promise { + const availableThemes = Object.keys(getAllThemes()); + + if (availableThemes.length === 0) { + ui.showWarning("No themes available to export"); + return; + } + + const themeToExport = + themeName || + (await select({ + message: "Select theme to export:", + choices: availableThemes.map((name) => ({ + name: chalk.cyan(name), + value: name, + })), + })); + + const theme = getTheme(themeToExport); + if (!theme) { + ui.showError(`Theme "${themeToExport}" not found`); + return; + } + + const defaultFilename = `${themeToExport.replace(/[^a-zA-Z0-9-_]/g, "-")}.theme.json`; + const filename = await input({ + message: "Export filename:", + default: defaultFilename, + validate: (value) => { + if (!value.trim()) return "Filename is required"; + if (!value.endsWith(".json")) return "Filename should end with .json"; + return true; + }, + }); + + try { + const exportData = { + ...theme, + exportedAt: new Date().toISOString(), + exportedBy: "LogsDX Theme Generator", + version: "1.0.0", + }; + + fs.writeFileSync(filename, JSON.stringify(exportData, null, 2)); + ui.showSuccess( + `Theme "${themeToExport}" exported to ${chalk.cyan(filename)}`, + ); + + const showPreview = await confirm({ + message: "Show file preview?", + default: false, + }); + + if (showPreview) { + console.log(chalk.dim("\nFile contents:")); + console.log(chalk.dim("─".repeat(50))); + console.log(JSON.stringify(exportData, null, 2)); + console.log(chalk.dim("─".repeat(50))); + } + } catch (error) { + ui.showError( + `Failed to export theme: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +export function exportThemeToFile( + theme: Theme, + filePath: string, + format: "json" | "typescript" = "json", +): void { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + if (format === "typescript") { + const tsContent = `import type { Theme } from "logsdx"; + +export const theme: Theme = ${JSON.stringify(theme, null, 2)}; + +export default theme; +`; + fs.writeFileSync(filePath, tsContent); + } else { + fs.writeFileSync(filePath, JSON.stringify(theme, null, 2)); + } +} + +export function importThemeFromFile(filePath: string): Theme { + const fileContent = fs.readFileSync(filePath, "utf8"); + + if (filePath.endsWith(".ts") || filePath.endsWith(".js")) { + const patterns = [ + /export\s+const\s+\w+\s*:\s*\w+\s*=\s*(\{[\s\S]*?\})\s*;?\s*$/m, + /export\s+default\s+(\{[\s\S]*?\})\s*;?\s*$/m, + /=\s*(\{[\s\S]*?\})\s*;?\s*export\s+default/m, + ]; + + for (const pattern of patterns) { + const match = fileContent.match(pattern); + if (match) { + try { + const jsonStr = match[1] + .replace(/^\s+/gm, "") + .replace(/\s+$/gm, "") + .trim(); + return JSON.parse(jsonStr); + } catch { + continue; + } + } + } + throw new Error( + `Failed to parse theme file: The TypeScript/JavaScript file does not contain a valid theme export. ` + + `Expected an export statement with a theme object containing 'name' and 'schema' properties.`, + ); + } + + const parsed = JSON.parse(fileContent); + if (!parsed.name || !parsed.schema) { + const missing = []; + if (!parsed.name) missing.push("'name'"); + if (!parsed.schema) missing.push("'schema'"); + throw new Error( + `Invalid theme JSON: Missing required fields: ${missing.join(", ")}. ` + + `Theme files must contain both a 'name' string and a 'schema' object.`, + ); + } + return parsed; +} + +export async function importTheme(filename?: string): Promise { + const themeFile = + filename || + (await input({ + message: "Theme file path:", + validate: (value) => { + if (!value.trim()) return "File path is required"; + if (!fs.existsSync(value)) return "File does not exist"; + if (!value.endsWith(".json")) return "File should be a JSON file"; + return true; + }, + })); + + try { + const fileContent = fs.readFileSync(themeFile, "utf8"); + const themeData = JSON.parse(fileContent); + + const validatedTheme = themePresetSchema.parse(themeData); + + ui.showInfo(`Importing theme: ${chalk.cyan(validatedTheme.name)}`); + if (validatedTheme.description) { + console.log(`Description: ${validatedTheme.description}`); + } + + const existingTheme = getTheme(validatedTheme.name); + if (existingTheme) { + const shouldOverwrite = await confirm({ + message: `Theme "${validatedTheme.name}" already exists. Overwrite?`, + default: false, + }); + + if (!shouldOverwrite) { + const newName = await input({ + message: "Enter a new name for the theme:", + default: `${validatedTheme.name}-imported`, + validate: (value) => (value.trim() ? true : "Name is required"), + }); + validatedTheme.name = newName; + } + } + + const showPreview = await confirm({ + message: "Preview theme before importing?", + default: true, + }); + + if (showPreview) { + await previewImportedTheme(validatedTheme); + } + + const shouldImport = await confirm({ + message: `Import theme "${validatedTheme.name}"?`, + default: true, + }); + + if (shouldImport) { + registerTheme(validatedTheme); + ui.showSuccess(`Theme "${validatedTheme.name}" imported successfully!`); + console.log( + chalk.green( + `\n✨ Use your imported theme with: logsdx --theme ${validatedTheme.name} your-log-file.log`, + ), + ); + } else { + ui.showInfo("Import cancelled"); + } + } catch (error) { + if (error instanceof Error) { + if (error.message.includes("JSON")) { + ui.showError( + "Invalid JSON file", + "Make sure the file contains valid JSON", + ); + } else if (error.message.includes("validation")) { + ui.showError( + "Invalid theme format", + "The file doesn't contain a valid LogsDX theme", + ); + } else { + ui.showError(`Import failed: ${error.message}`); + } + } else { + ui.showError("Import failed", String(error)); + } + } +} + +async function previewImportedTheme(theme: Theme) { + console.log(chalk.bold("\n🎬 Theme Preview:\n")); + + const sampleLogs = [ + "2024-01-15 10:30:45 INFO Starting application server", + "2024-01-15 10:30:46 DEBUG Loading configuration files", + "2024-01-15 10:30:47 WARN Memory usage is high: 85%", + "2024-01-15 10:30:48 ERROR Database connection failed", + "GET /api/users 200 OK 45ms", + "POST /api/login 401 Unauthorized 23ms", + ]; + + registerTheme(theme); + const logsDX = LogsDX.getInstance({ + theme: theme.name, + outputFormat: "ansi", + }); + + sampleLogs.forEach((log) => { + console.log(` ${logsDX.processLine(log)}`); + }); + + console.log(chalk.bold("\n📋 Theme Details:")); + console.log(` Name: ${chalk.cyan(theme.name)}`); + if (theme.description) { + console.log(` Description: ${theme.description}`); + } + if ("exportedAt" in theme && (theme as any).exportedAt) { + console.log( + ` Exported: ${chalk.dim(new Date((theme as any).exportedAt).toLocaleString())}`, + ); + } + + const wordCount = Object.keys(theme.schema.matchWords || {}).length; + const patternCount = (theme.schema.matchPatterns || []).length; + console.log( + ` Patterns: ${chalk.yellow(patternCount)}, Words: ${chalk.yellow(wordCount)}`, + ); +} + +export function getThemeFiles(directory = "."): string[] { + try { + if (!fs.existsSync(directory)) { + return []; + } + + const files: string[] = []; + + function scanDir(dir: string) { + const items = fs.readdirSync(dir, { withFileTypes: true }); + for (const item of items) { + const fullPath = path.join(dir, item.name); + if (item.isDirectory()) { + scanDir(fullPath); + } else if ( + item.name.endsWith(".theme.json") || + item.name.endsWith(".theme.ts") || + (item.name.includes("theme") && + (item.name.endsWith(".json") || item.name.endsWith(".ts"))) + ) { + files.push(fullPath); + } + } + } + + scanDir(directory); + return files; + } catch { + return []; + } +} + +export { getThemeFiles as listThemeFiles }; + +export function listThemeFilesCommand(directory = "."): void { + try { + const files = fs + .readdirSync(directory) + .filter((file) => file.endsWith(".theme.json")) + .map((file) => path.join(directory, file)); + + if (files.length === 0) { + ui.showInfo("No theme files found in current directory"); + console.log( + chalk.dim("Theme files should have the extension .theme.json"), + ); + return; + } + + ui.showInfo(`Found ${files.length} theme file(s):\n`); + + files.forEach((file, index) => { + try { + const content = fs.readFileSync(file, "utf8"); + const themeData = JSON.parse(content); + + console.log(chalk.bold.cyan(`${index + 1}. ${path.basename(file)}`)); + console.log(` Theme: ${themeData.name || "Unknown"}`); + if (themeData.description) { + console.log(` Description: ${themeData.description}`); + } + if (themeData.exportedAt) { + console.log( + ` Exported: ${chalk.dim(new Date(themeData.exportedAt).toLocaleString())}`, + ); + } + console.log(` File: ${chalk.dim(file)}`); + console.log(); + } catch { + console.log(chalk.bold.red(`${index + 1}. ${path.basename(file)}`)); + console.log(chalk.red(` Error: Invalid theme file`)); + console.log(` File: ${chalk.dim(file)}`); + console.log(); + } + }); + + console.log( + chalk.yellow("💡 Use --import-theme to import a theme"), + ); + } catch (error) { + ui.showError( + `Failed to list theme files: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} diff --git a/src/cli/theme/transporter.ts b/src/cli/theme/transporter.ts deleted file mode 100644 index 71d1df5..0000000 --- a/src/cli/theme/transporter.ts +++ /dev/null @@ -1,354 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { ui } from "../ui"; -import { select, input, confirm } from "../../utils/prompts"; -import { registerTheme, getAllThemes, getTheme } from "../../themes"; -import type { Theme } from "../../types"; -import { themePresetSchema } from "../../schema"; -import chalk from "chalk"; - -export async function exportTheme(themeName?: string): Promise { - const availableThemes = Object.keys(getAllThemes()); - - if (availableThemes.length === 0) { - ui.showWarning("No themes available to export"); - return; - } - - const themeToExport = - themeName || - (await select({ - message: "Select theme to export:", - choices: availableThemes.map((name) => ({ - name: chalk.cyan(name), - value: name, - })), - })); - - const theme = getTheme(themeToExport); - if (!theme) { - ui.showError(`Theme "${themeToExport}" not found`); - return; - } - - const defaultFilename = `${themeToExport.replace(/[^a-zA-Z0-9-_]/g, "-")}.theme.json`; - const filename = await input({ - message: "Export filename:", - default: defaultFilename, - validate: (value) => { - if (!value.trim()) return "Filename is required"; - if (!value.endsWith(".json")) return "Filename should end with .json"; - return true; - }, - }); - - try { - const exportData = { - ...theme, - exportedAt: new Date().toISOString(), - exportedBy: "LogsDX Theme Generator", - version: "1.0.0", - }; - - fs.writeFileSync(filename, JSON.stringify(exportData, null, 2)); - ui.showSuccess( - `Theme "${themeToExport}" exported to ${chalk.cyan(filename)}`, - ); - - const showPreview = await confirm({ - message: "Show file preview?", - default: false, - }); - - if (showPreview) { - console.log(chalk.dim("\nFile contents:")); - console.log(chalk.dim("─".repeat(50))); - console.log(JSON.stringify(exportData, null, 2)); - console.log(chalk.dim("─".repeat(50))); - } - } catch (error) { - ui.showError( - `Failed to export theme: ${error instanceof Error ? error.message : String(error)}`, - ); - } -} - -export function exportThemeToFile( - theme: Theme, - filePath: string, - format: "json" | "typescript" = "json", -): void { - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - if (format === "typescript") { - const tsContent = `import type { Theme } from "logsdx"; - -export const theme: Theme = ${JSON.stringify(theme, null, 2)}; - -export default theme; -`; - fs.writeFileSync(filePath, tsContent); - } else { - fs.writeFileSync(filePath, JSON.stringify(theme, null, 2)); - } -} - -export function importThemeFromFile(filePath: string): Theme { - const fileContent = fs.readFileSync(filePath, "utf8"); - - if (filePath.endsWith(".ts") || filePath.endsWith(".js")) { - const patterns = [ - /export\s+const\s+\w+\s*:\s*\w+\s*=\s*(\{[\s\S]*?\})\s*;?\s*$/m, - /export\s+default\s+(\{[\s\S]*?\})\s*;?\s*$/m, - /=\s*(\{[\s\S]*?\})\s*;?\s*export\s+default/m, - ]; - - for (const pattern of patterns) { - const match = fileContent.match(pattern); - if (match) { - try { - const jsonStr = match[1] - .replace(/^\s+/gm, "") - .replace(/\s+$/gm, "") - .trim(); - return JSON.parse(jsonStr); - } catch { - continue; - } - } - } - throw new Error( - `Failed to parse theme file: The TypeScript/JavaScript file does not contain a valid theme export. ` + - `Expected an export statement with a theme object containing 'name' and 'schema' properties.`, - ); - } - - const parsed = JSON.parse(fileContent); - if (!parsed.name || !parsed.schema) { - const missing = []; - if (!parsed.name) missing.push("'name'"); - if (!parsed.schema) missing.push("'schema'"); - throw new Error( - `Invalid theme JSON: Missing required fields: ${missing.join(", ")}. ` + - `Theme files must contain both a 'name' string and a 'schema' object.`, - ); - } - return parsed; -} - -export async function importTheme(filename?: string): Promise { - const themeFile = - filename || - (await input({ - message: "Theme file path:", - validate: (value) => { - if (!value.trim()) return "File path is required"; - if (!fs.existsSync(value)) return "File does not exist"; - if (!value.endsWith(".json")) return "File should be a JSON file"; - return true; - }, - })); - - try { - const fileContent = fs.readFileSync(themeFile, "utf8"); - const themeData = JSON.parse(fileContent); - - const validatedTheme = themePresetSchema.parse(themeData); - - ui.showInfo(`Importing theme: ${chalk.cyan(validatedTheme.name)}`); - if (validatedTheme.description) { - console.log(`Description: ${validatedTheme.description}`); - } - - const existingTheme = getTheme(validatedTheme.name); - if (existingTheme) { - const shouldOverwrite = await confirm({ - message: `Theme "${validatedTheme.name}" already exists. Overwrite?`, - default: false, - }); - - if (!shouldOverwrite) { - const newName = await input({ - message: "Enter a new name for the theme:", - default: `${validatedTheme.name}-imported`, - validate: (value) => (value.trim() ? true : "Name is required"), - }); - validatedTheme.name = newName; - } - } - - const showPreview = await confirm({ - message: "Preview theme before importing?", - default: true, - }); - - if (showPreview) { - await previewImportedTheme(validatedTheme); - } - - const shouldImport = await confirm({ - message: `Import theme "${validatedTheme.name}"?`, - default: true, - }); - - if (shouldImport) { - registerTheme(validatedTheme); - ui.showSuccess(`Theme "${validatedTheme.name}" imported successfully!`); - console.log( - chalk.green( - `\n✨ Use your imported theme with: logsdx --theme ${validatedTheme.name} your-log-file.log`, - ), - ); - } else { - ui.showInfo("Import cancelled"); - } - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("JSON")) { - ui.showError( - "Invalid JSON file", - "Make sure the file contains valid JSON", - ); - } else if (error.message.includes("validation")) { - ui.showError( - "Invalid theme format", - "The file doesn't contain a valid LogsDX theme", - ); - } else { - ui.showError(`Import failed: ${error.message}`); - } - } else { - ui.showError("Import failed", String(error)); - } - } -} - -async function previewImportedTheme(theme: Theme) { - console.log(chalk.bold("\n🎬 Theme Preview:\n")); - - const sampleLogs = [ - "2024-01-15 10:30:45 INFO Starting application server", - "2024-01-15 10:30:46 DEBUG Loading configuration files", - "2024-01-15 10:30:47 WARN Memory usage is high: 85%", - "2024-01-15 10:30:48 ERROR Database connection failed", - "GET /api/users 200 OK 45ms", - "POST /api/login 401 Unauthorized 23ms", - ]; - - const { LogsDX } = await import("../../index"); - registerTheme(theme); - const logsDX = LogsDX.getInstance({ - theme: theme.name, - outputFormat: "ansi", - }); - - sampleLogs.forEach((log) => { - console.log(` ${logsDX.processLine(log)}`); - }); - - console.log(chalk.bold("\n📋 Theme Details:")); - console.log(` Name: ${chalk.cyan(theme.name)}`); - if (theme.description) { - console.log(` Description: ${theme.description}`); - } - if ("exportedAt" in theme && (theme as any).exportedAt) { - console.log( - ` Exported: ${chalk.dim(new Date((theme as any).exportedAt).toLocaleString())}`, - ); - } - - const wordCount = Object.keys(theme.schema.matchWords || {}).length; - const patternCount = (theme.schema.matchPatterns || []).length; - console.log( - ` Patterns: ${chalk.yellow(patternCount)}, Words: ${chalk.yellow(wordCount)}`, - ); -} - -export function getThemeFiles(directory = "."): string[] { - try { - if (!fs.existsSync(directory)) { - return []; - } - - const files: string[] = []; - - function scanDir(dir: string) { - const items = fs.readdirSync(dir, { withFileTypes: true }); - for (const item of items) { - const fullPath = path.join(dir, item.name); - if (item.isDirectory()) { - scanDir(fullPath); - } else if ( - item.name.endsWith(".theme.json") || - item.name.endsWith(".theme.ts") || - (item.name.includes("theme") && - (item.name.endsWith(".json") || item.name.endsWith(".ts"))) - ) { - files.push(fullPath); - } - } - } - - scanDir(directory); - return files; - } catch { - return []; - } -} - -export { getThemeFiles as listThemeFiles }; - -export function listThemeFilesCommand(directory = "."): void { - try { - const files = fs - .readdirSync(directory) - .filter((file) => file.endsWith(".theme.json")) - .map((file) => path.join(directory, file)); - - if (files.length === 0) { - ui.showInfo("No theme files found in current directory"); - console.log( - chalk.dim("Theme files should have the extension .theme.json"), - ); - return; - } - - ui.showInfo(`Found ${files.length} theme file(s):\n`); - - files.forEach((file, index) => { - try { - const content = fs.readFileSync(file, "utf8"); - const themeData = JSON.parse(content); - - console.log(chalk.bold.cyan(`${index + 1}. ${path.basename(file)}`)); - console.log(` Theme: ${themeData.name || "Unknown"}`); - if (themeData.description) { - console.log(` Description: ${themeData.description}`); - } - if (themeData.exportedAt) { - console.log( - ` Exported: ${chalk.dim(new Date(themeData.exportedAt).toLocaleString())}`, - ); - } - console.log(` File: ${chalk.dim(file)}`); - console.log(); - } catch { - console.log(chalk.bold.red(`${index + 1}. ${path.basename(file)}`)); - console.log(chalk.red(` Error: Invalid theme file`)); - console.log(` File: ${chalk.dim(file)}`); - console.log(); - } - }); - - console.log( - chalk.yellow("💡 Use --import-theme to import a theme"), - ); - } catch (error) { - ui.showError( - `Failed to list theme files: ${error instanceof Error ? error.message : String(error)}`, - ); - } -} diff --git a/src/themes/builder.ts b/src/themes/builder.ts index 1fc19af..83ba414 100644 --- a/src/themes/builder.ts +++ b/src/themes/builder.ts @@ -229,12 +229,12 @@ export const THEME_PRESETS: Record = { patterns: [ { name: "unix-path", - pattern: /\/[\w\-\.\/]+/g, + pattern: /\/[\w\-./]+/g, options: { color: "muted" }, }, { name: "windows-path", - pattern: /[A-Z]:\\[\w\-\.\\]+/g, + pattern: /[A-Z]:\\[\w\-.\\]+/g, options: { color: "muted" }, }, ], diff --git a/src/themes/color-constants.ts b/src/themes/colors.ts similarity index 100% rename from src/themes/color-constants.ts rename to src/themes/colors.ts diff --git a/src/themes/template.ts b/src/themes/presets.ts similarity index 100% rename from src/themes/template.ts rename to src/themes/presets.ts diff --git a/src/themes/utils.ts b/src/themes/utils.ts index aea1aa5..f458602 100644 --- a/src/themes/utils.ts +++ b/src/themes/utils.ts @@ -1,4 +1,4 @@ -import { colors } from "./color-constants"; +import { colors } from "./colors"; export function isDarkColor(hex: string): boolean { const color = hex.replace("#", ""); diff --git a/src/utils/cli/constants.ts b/src/utils/cli/constants.ts deleted file mode 100644 index b45a080..0000000 --- a/src/utils/cli/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const HELP_FLAGS = ["--help", "-h"]; -export const VERSION_FLAGS = ["--version", "-v"]; - -export const BOOLEAN_FLAG_PREFIX = "no-"; diff --git a/src/utils/cli/types.ts b/src/utils/cli/types.ts deleted file mode 100644 index a0e8250..0000000 --- a/src/utils/cli/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface OptionDefinition { - flags: string; - description: string; - defaultValue?: any; -} - -export interface ParsedOptions { - [key: string]: any; -} - -export interface ArgumentDefinition { - name: string; - description: string; - required: boolean; -} diff --git a/src/utils/cli/utils.ts b/src/utils/cli/utils.ts deleted file mode 100644 index a38505a..0000000 --- a/src/utils/cli/utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { OptionDefinition } from "./types"; - -export function camelCase(str: string): string { - return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); -} - -export function findOption( - options: OptionDefinition[], - flag: string, -): OptionDefinition | undefined { - return options.find((o) => o.flags.includes(flag)); -} - -export function extractLongFlag(flags: string): string | null { - const match = flags.match(/--([a-z-]+)/); - return match ? match[1] : null; -} - -export function expectsValue(flags: string): boolean { - return flags.includes("<"); -} - -export function hasOptionalValue(flags: string): boolean { - return flags.includes("[") && !flags.startsWith("["); -} - -export function isBooleanFlag(flags: string): boolean { - return !expectsValue(flags) && !hasOptionalValue(flags); -} diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts new file mode 100644 index 0000000..c357d51 --- /dev/null +++ b/src/utils/formatting.ts @@ -0,0 +1,192 @@ +// ============================================================================ +// ASCII Art (from utils/ascii.ts) +// ============================================================================ + +const LOGSDX_ASCII = ` + ╦ ┌─┐┌─┐┌─┐╔╦╗═╗ ╦ + ║ │ ││ ┬└─┐ ║║╔╩╦╝ + ╩═╝└─┘└─┘└─┘═╩╝╩ ╚═ +`; + +export function textSync( + text: string, + options?: { + font?: string; + horizontalLayout?: string; + verticalLayout?: string; + }, +): string { + if (text === "LogsDX") { + return LOGSDX_ASCII; + } + + return text; +} + +// ============================================================================ +// Boxen (from utils/boxen.ts) +// ============================================================================ + +interface BoxenOptions { + padding?: + | number + | { top?: number; bottom?: number; left?: number; right?: number }; + margin?: + | number + | { top?: number; bottom?: number; left?: number; right?: number }; + borderStyle?: "single" | "double" | "round" | "bold" | "classic"; + borderColor?: string; + backgroundColor?: string; + title?: string; +} + +const borderStyles = { + single: { + topLeft: "┌", + topRight: "┐", + bottomLeft: "└", + bottomRight: "┘", + horizontal: "─", + vertical: "│", + }, + double: { + topLeft: "╔", + topRight: "╗", + bottomLeft: "╚", + bottomRight: "╝", + horizontal: "═", + vertical: "║", + }, + round: { + topLeft: "╭", + topRight: "╮", + bottomLeft: "╰", + bottomRight: "╯", + horizontal: "─", + vertical: "│", + }, + bold: { + topLeft: "┏", + topRight: "┓", + bottomLeft: "┗", + bottomRight: "┛", + horizontal: "━", + vertical: "┃", + }, + classic: { + topLeft: "+", + topRight: "+", + bottomLeft: "+", + bottomRight: "+", + horizontal: "-", + vertical: "|", + }, +}; + +function normalizePadding( + value: + | number + | { top?: number; bottom?: number; left?: number; right?: number } + | undefined, +): { top: number; bottom: number; left: number; right: number } { + if (typeof value === "number") { + return { top: value, bottom: value, left: value, right: value }; + } + return { + top: value?.top || 0, + bottom: value?.bottom || 0, + left: value?.left || 0, + right: value?.right || 0, + }; +} + +export function boxen(text: string, options: BoxenOptions = {}): string { + const border = borderStyles[options.borderStyle || "single"]; + const padding = normalizePadding(options.padding); + const margin = normalizePadding(options.margin); + + const lines = text.split("\n"); + const contentWidth = Math.max( + ...lines.map((line) => line.replace(/\x1B\[[0-9;]*m/g, "").length), + ); + const boxWidth = contentWidth + padding.left + padding.right; + + const result: string[] = []; + + for (let i = 0; i < margin.top; i++) { + result.push(""); + } + + const leftMargin = " ".repeat(margin.left); + + const topBorder = options.title + ? border.topLeft + + ` ${options.title} ` + + border.horizontal.repeat( + Math.max(0, boxWidth - options.title.length - 2), + ) + + border.topRight + : border.topLeft + border.horizontal.repeat(boxWidth) + border.topRight; + result.push(leftMargin + topBorder); + + for (let i = 0; i < padding.top; i++) { + result.push( + leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical, + ); + } + + lines.forEach((line) => { + const cleanLength = line.replace(/\x1B\[[0-9;]*m/g, "").length; + const paddingRight = " ".repeat(Math.max(0, contentWidth - cleanLength)); + result.push( + leftMargin + + border.vertical + + " ".repeat(padding.left) + + line + + paddingRight + + " ".repeat(padding.right) + + border.vertical, + ); + }); + + for (let i = 0; i < padding.bottom; i++) { + result.push( + leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical, + ); + } + + result.push( + leftMargin + + border.bottomLeft + + border.horizontal.repeat(boxWidth) + + border.bottomRight, + ); + + for (let i = 0; i < margin.bottom; i++) { + result.push(""); + } + + return result.join("\n"); +} + +// ============================================================================ +// Gradient (from utils/gradient.ts) +// ============================================================================ + +export function gradient(colors: string[]): { + (text: string): string; + multiline(text: string): string; +} { + const applyGradient = (text: string) => `\x1B[36m${text}\x1B[0m`; + + applyGradient.multiline = (text: string) => { + return text + .split("\n") + .map((line) => `\x1B[36m${line}\x1B[0m`) + .join("\n"); + }; + + return applyGradient; +} + +export default { textSync, boxen, gradient }; diff --git a/src/utils/prompts.ts b/src/utils/prompts.ts index 8025063..7cd47a2 100644 --- a/src/utils/prompts.ts +++ b/src/utils/prompts.ts @@ -1,5 +1,4 @@ import * as readline from "readline"; -import colors from "./colors"; import { logger } from "./logger"; interface BasePrompt { diff --git a/src/utils/terminal.ts b/src/utils/terminal.ts new file mode 100644 index 0000000..95be1c8 --- /dev/null +++ b/src/utils/terminal.ts @@ -0,0 +1,154 @@ +import colors from "./colors"; + +// ============================================================================ +// Logger (from utils/logger.ts) +// ============================================================================ + +export const logger = { + info(message: string) { + console.log(colors.blue("ℹ"), message); + }, + + success(message: string) { + console.log(colors.green("✔"), message); + }, + + warn(message: string) { + console.log(colors.yellow("⚠"), message); + }, + + error(message: string) { + console.error(colors.red("✖"), message); + }, + + debug(message: string) { + console.log(colors.gray("⚙"), message); + }, +}; + +// ============================================================================ +// Spinner (from utils/spinner.ts) +// ============================================================================ + +export interface Spinner { + start(): Spinner; + succeed(text?: string): Spinner; + fail(text?: string): Spinner; + stop(): Spinner; + text: string; +} + +const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +export function spinner(initialText: string): Spinner { + let text = initialText; + let frameIndex = 0; + let interval: ReturnType | null = null; + let isSpinning = false; + + const instance: Spinner = { + get text() { + return text; + }, + set text(value: string) { + text = value; + }, + + start() { + if (isSpinning) return instance; + + isSpinning = true; + process.stdout.write("\x1B[?25l"); + + interval = setInterval(() => { + const frame = frames[frameIndex]; + frameIndex = (frameIndex + 1) % frames.length; + process.stdout.write(`\r\x1B[36m${frame}\x1B[0m ${text}`); + }, 80); + + return instance; + }, + + succeed(successText?: string) { + instance.stop(); + const message = successText || text; + process.stdout.write(`\r\x1B[32m✔\x1B[0m ${message}\n`); + return instance; + }, + + fail(failText?: string) { + instance.stop(); + const message = failText || text; + process.stdout.write(`\r\x1B[31m✖\x1B[0m ${message}\n`); + return instance; + }, + + stop() { + if (interval) { + clearInterval(interval); + interval = null; + } + if (isSpinning) { + process.stdout.write("\r\x1B[K"); + process.stdout.write("\x1B[?25h"); + isSpinning = false; + } + return instance; + }, + }; + + return instance; +} + +// ============================================================================ +// Progress Bar (from utils/progress.ts) +// ============================================================================ + +export interface ProgressBar { + start(total: number, startValue: number): void; + update(value: number): void; + stop(): void; +} + +export function createProgressBar(total: number): ProgressBar { + let currentValue = 0; + let startTime = Date.now(); + let isRunning = false; + + const render = (value: number) => { + const percentage = Math.floor((value / total) * 100); + const barLength = 40; + const filledLength = Math.floor((percentage / 100) * barLength); + const bar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength); + const elapsed = Date.now() - startTime; + const duration = `${Math.floor(elapsed / 1000)}s`; + + process.stdout.write( + `\r ${bar} | ${percentage}% | ${value}/${total} lines | ${duration}`, + ); + }; + + return { + start(totalValue: number, startValue: number) { + total = totalValue; + currentValue = startValue; + startTime = Date.now(); + isRunning = true; + render(currentValue); + }, + + update(value: number) { + if (!isRunning) return; + currentValue = value; + render(currentValue); + }, + + stop() { + if (!isRunning) return; + isRunning = false; + process.stdout.write("\n"); + }, + }; +} + +export default { logger, spinner, createProgressBar }; diff --git a/tests/unit/cli/theme/generator.test.ts b/tests/unit/cli/theme/generator.test.ts index 04d9004..0a5ca3d 100644 --- a/tests/unit/cli/theme/generator.test.ts +++ b/tests/unit/cli/theme/generator.test.ts @@ -5,11 +5,11 @@ import { generatePatternFromPreset, listColorPalettesCommand, listPatternPresetsCommand, -} from "../../../../src/cli/theme/generator"; +} from "../../../../src/cli/theme-gen"; import { COLOR_PALETTES, PATTERN_PRESETS, -} from "../../../../src/themes/template"; +} from "../../../../src/themes/presets"; const originalLog = console.log; const originalWarn = console.warn; diff --git a/tests/unit/cli/theme/transporter.test.ts b/tests/unit/cli/theme/transporter.test.ts index 925b683..1907c8d 100644 --- a/tests/unit/cli/theme/transporter.test.ts +++ b/tests/unit/cli/theme/transporter.test.ts @@ -7,7 +7,7 @@ import { listThemeFiles, getThemeFiles, listThemeFilesCommand, -} from "../../../../src/cli/theme/transporter"; +} from "../../../../src/cli/theme-gen"; import type { Theme } from "../../../../src/types"; const originalLog = console.log; diff --git a/tests/unit/themes/template.test.ts b/tests/unit/themes/presets.test.ts similarity index 99% rename from tests/unit/themes/template.test.ts rename to tests/unit/themes/presets.test.ts index ed779e2..682497a 100644 --- a/tests/unit/themes/template.test.ts +++ b/tests/unit/themes/presets.test.ts @@ -10,7 +10,7 @@ import { generateTemplate, COLOR_PALETTES, PATTERN_PRESETS, -} from "../../../src/themes/template"; +} from "../../../src/themes/presets"; describe("Theme Generator Schemas", () => { test("colorPaletteSchema should validate valid palette", () => { From 6efa09e875cac45ae80029438b131c55e6f2849a Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:19:17 -0800 Subject: [PATCH 6/6] chore: fixes parser --- src/cli/parser.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli/parser.ts b/src/cli/parser.ts index 480d0b2..73a42a9 100644 --- a/src/cli/parser.ts +++ b/src/cli/parser.ts @@ -72,7 +72,11 @@ export class CLI { return this; } - option(flags: string, description: string, defaultValue?: string | boolean | number): this { + option( + flags: string, + description: string, + defaultValue?: string | boolean | number, + ): this { this.options.push({ flags, description, defaultValue }); return this; }