diff --git a/LICENSE b/LICENSE index 267df56..bf0f4de 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 waaraawa +Copyright (c) 2026 waaraawa Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/core/package.json b/packages/core/package.json index 5cbfa09..65e73dc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,10 +17,8 @@ ], "author": "ByteGrid Contributors", "license": "MIT", - "dependencies": { - "js-yaml": "^4.1.0" - }, "devDependencies": { - "@types/js-yaml": "^4.0.9" + "@types/js-yaml": "^4.0.9", + "js-yaml": "^4.1.0" } } diff --git a/packages/core/src/parser.ts b/packages/core/src/parser.ts index 280e0da..9d51686 100644 --- a/packages/core/src/parser.ts +++ b/packages/core/src/parser.ts @@ -3,28 +3,16 @@ * Parses YAML input to ByteGridConfig */ -import * as yaml from 'js-yaml'; import { ByteGridConfig, Field, DataType } from './types'; import { ParseError } from './errors'; /** * Parse YAML string to ByteGridConfig - * @param source YAML string + * @param parsed Parsed YAML object * @returns Parsed ByteGridConfig * @throws ParseError if parsing fails or required fields are missing */ -export function parse(source: string): ByteGridConfig { - let parsed: unknown; - - // Parse YAML - try { - parsed = yaml.load(source); - } catch (error) { - throw new ParseError( - `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}` - ); - } - +export function parse(parsed: unknown): ByteGridConfig { // Validate parsed result is an object if (!parsed || typeof parsed !== 'object') { throw new ParseError('Invalid YAML: expected an object'); diff --git a/packages/core/src/svgRenderer.ts b/packages/core/src/svgRenderer.ts index acc1e9f..0aa187e 100644 --- a/packages/core/src/svgRenderer.ts +++ b/packages/core/src/svgRenderer.ts @@ -3,7 +3,7 @@ * Renders LayoutBlocks to SVG string */ -import { ByteGridConfig, LayoutBlock, RenderOptions, ColorName, LegendPosition, ColorScheme } from './types'; +import { ByteGridConfig, LayoutBlock, RenderOptions, ColorName, LegendPosition, ColorScheme, CustomTheme } from './types'; /** * Color palette mapping (default scheme) @@ -115,6 +115,12 @@ function rgbToHex(r: number, g: number, b: number): string { * Transform color based on color scheme */ function transformColor(hexColor: string, scheme: ColorScheme): string { + // If the color is a CSS variable, we can't transform it via JS, + // we rely on the theme to provide the correct dark/light variants natively. + if (hexColor.startsWith('var(')) { + return hexColor; + } + if (scheme === 'default') { return hexColor; } @@ -150,6 +156,15 @@ const DEFAULT_OPTIONS: Required = { cellHeight: 30, fontSize: 10, uniformRowHeight: false, // Bitfield rows are taller by default + theme: { + background: 'white', + textNormal: '#333', + textMuted: '#666', + textFaint: '#999', + border: '#333', + gridLine: '#ccc', + gridLineSubtle: '#ddd', + } }; /** @@ -166,6 +181,15 @@ export function renderSVG( options?: RenderOptions ): string { const opts = { ...DEFAULT_OPTIONS, ...options }; + const theme: CustomTheme = opts.theme || { + background: 'white', + textNormal: '#333', + textMuted: '#666', + textFaint: '#999', + border: '#333', + gridLine: '#ccc', + gridLineSubtle: '#ddd', + }; const layoutUnit = config.layoutUnit || 'byte'; const layout = config.layout || 16; @@ -338,10 +362,10 @@ export function renderSVG( return block.bitfields && block.bitfields.length > 0 ? bitfieldCellHeight : normalCellHeight; }; - let svg = ``; + let svg = ``; // Title - svg += `${escapeXml(config.name)}`; + svg += `${escapeXml(config.name)}`; // Column headers if (opts.showGrid) { @@ -352,7 +376,7 @@ export function renderSVG( const headerLabel = layoutUnit === 'bit' ? col % 8 : '0x' + col.toString(16).toUpperCase().padStart(2, '0'); - svg += `${headerLabel}`; + svg += `${headerLabel}`; } } @@ -366,7 +390,7 @@ export function renderSVG( const startOffset = row * layout; const hexOffset = '0x' + startOffset.toString(16).toUpperCase().padStart(2, '0'); const textY = y + cellHeight / 2 + 4; - svg += `${hexOffset}`; + svg += `${hexOffset}`; } } @@ -376,11 +400,11 @@ export function renderSVG( const y = rowYPositions.get(block.row) || gridStartY; const width = block.span * opts.cellWidth; const cellHeight = getCellHeight(block); - const baseColor = COLORS[block.color] || COLORS.gray; + const baseColor = theme.palette?.[block.color] || COLORS[block.color] || COLORS.gray; const color = transformColor(baseColor, effectiveColorScheme); // Draw block rectangle - svg += ``; + svg += ``; // Draw numbers inside cells (only for byte layout) if (layoutUnit !== 'bit') { @@ -389,7 +413,8 @@ export function renderSVG( const hexNum = '0x' + byteNum.toString(16).toUpperCase().padStart(2, '0'); const cellX = x + i * opts.cellWidth + opts.cellWidth / 2; const cellY = y + (cellHeight / 3); // Upper third for byte number - svg += `${hexNum}`; + const textFill = theme.cellText || theme.textNormal; + svg += `${hexNum}`; } } @@ -402,14 +427,15 @@ export function renderSVG( // Draw vertical lines dividing 8 bits for (let bit = 1; bit < 8; bit++) { const lineX = cellX + bit * bitCellWidth; - svg += ``; + svg += ``; } // Draw bit numbers (0-7) for (let bit = 0; bit < 8; bit++) { const bitX = cellX + bit * bitCellWidth + bitCellWidth / 2; const bitY = y + (cellHeight * 2 / 3) + 5; // Lower third for bit numbers - svg += `${7 - bit}`; + const bitTextFill = theme.cellTextMuted || theme.textMuted; + svg += `${7 - bit}`; } } } @@ -422,9 +448,9 @@ export function renderSVG( // Every 8 bits: darker dotted line (byte boundary) // Other bits: lighter dotted line (bit boundary) if (col % 8 === 0) { - svg += ``; + svg += ``; } else { - svg += ``; + svg += ``; } } } @@ -451,7 +477,7 @@ export function renderSVG( legendTitleY = legendStartY - 20; } - svg += `Fields`; + svg += `Fields`; // Group blocks by field name to avoid duplicates const uniqueFields = new Map(); @@ -501,17 +527,17 @@ export function renderSVG( const x = legendX + col * legendColumnWidth; const y = rowYPositions[row]; - const baseColor = COLORS[block.color] || COLORS.gray; + const baseColor = theme.palette?.[block.color] || COLORS[block.color] || COLORS.gray; const color = transformColor(baseColor, effectiveColorScheme); // Color box - svg += ``; + svg += ``; // Field name - svg += `${escapeXml(fieldName)}`; + svg += `${escapeXml(fieldName)}`; // Type - svg += `${escapeXml(block.fieldType)}`; + svg += `${escapeXml(block.fieldType)}`; // Offset info const unit = layoutUnit === 'bit' ? 'bits' : 'bytes'; @@ -523,15 +549,15 @@ export function renderSVG( const offsetEndStr = layoutUnit === 'bit' ? block.offsetEnd.toString() : '0x' + block.offsetEnd.toString(16).toUpperCase().padStart(2, '0'); - svg += `offset: ${offsetStartStr}-${offsetEndStr} (${size} ${unit})`; + svg += `offset: ${offsetStartStr}-${offsetEndStr} (${size} ${unit})`; // Bitfields (if any) if (block.bitfields && block.bitfields.length > 0) { - svg += `Bits:`; + svg += `Bits:`; block.bitfields.forEach((bf, bfIndex) => { const bfY = y + 60 + bfIndex * 12; - svg += ``; + svg += ``; svg += `bit ${escapeXml(bf.bits)}: ${escapeXml(bf.name)}`; svg += ``; }); @@ -546,7 +572,7 @@ export function renderSVG( const footerY = totalHeight - 40; const sizeUnit = layoutUnit === 'bit' ? 'bits' : 'bytes'; const layoutUnitText = layoutUnit === 'bit' ? 'bits/row' : 'bytes/row'; - svg += `Total size: ${config.size} ${sizeUnit} | Layout: ${layout} ${layoutUnitText}`; + svg += `Total size: ${config.size} ${sizeUnit} | Layout: ${layout} ${layoutUnitText}`; } svg += ``; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index db5109c..bce8e89 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -132,6 +132,22 @@ export interface LayoutBlock { bitfields?: Bitfield[]; } +/** + * Custom theme variables for rendering (e.g., for Obsidian integration) + */ +export interface CustomTheme { + background: string; + textNormal: string; + textMuted: string; + textFaint: string; + border: string; + gridLine: string; + gridLineSubtle: string; + cellText?: string; + cellTextMuted?: string; + palette?: Partial>; +} + /** * Rendering options */ @@ -147,4 +163,5 @@ export interface RenderOptions { cellHeight?: number; fontSize?: number; uniformRowHeight?: boolean; // If true, all rows have same height. If false (default), bitfield rows are taller + theme?: CustomTheme; // Custom theme overrides } diff --git a/packages/core/tests/parser.test.ts b/packages/core/tests/parser.test.ts index 0fe4ea0..38bd4e3 100644 --- a/packages/core/tests/parser.test.ts +++ b/packages/core/tests/parser.test.ts @@ -3,9 +3,20 @@ * Tests for parsing YAML input to ByteGridConfig */ -import { parse } from '../src/parser'; +import * as yamlModule from 'js-yaml'; +import { parse as coreParse } from '../src/parser'; import { ParseError } from '../src/errors'; +function parse(source: string) { + try { + return coreParse(yamlModule.load(source)); + } catch(error) { + throw new ParseError( + `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + describe('Parser', () => { describe('parse()', () => { it('should parse valid YAML with minimal fields', () => { diff --git a/packages/obsidian-plugin/.env.example b/packages/obsidian-plugin/.env.example new file mode 100644 index 0000000..01e46a4 --- /dev/null +++ b/packages/obsidian-plugin/.env.example @@ -0,0 +1,2 @@ +# Configure your absolute path to the Obsidian plugin vault below for automatic copying on build +# OBSIDIAN_PLUGIN_DIR=/path/to/your/vault/.obsidian/plugins/bytegrid diff --git a/packages/obsidian-plugin/esbuild.config.mjs b/packages/obsidian-plugin/esbuild.config.mjs index a9fa855..d5cef13 100644 --- a/packages/obsidian-plugin/esbuild.config.mjs +++ b/packages/obsidian-plugin/esbuild.config.mjs @@ -14,7 +14,16 @@ const prod = process.argv[2] === 'production'; const watch = process.argv[2] === '--watch'; // Obsidian vault plugin directory -const OBSIDIAN_PLUGIN_DIR = '/Users/jinjung/Dropbox/obsidian/.obsidian/plugins/bytegrid'; +let OBSIDIAN_PLUGIN_DIR = process.env.OBSIDIAN_PLUGIN_DIR; +try { + const envContent = fs.readFileSync('.env', 'utf8'); + const match = envContent.match(/^OBSIDIAN_PLUGIN_DIR=(.*)$/m); + if (match) { + OBSIDIAN_PLUGIN_DIR = match[1].trim(); + } +} catch (e) { + // Ignore if .env doesn't exist +} // Plugin to copy files to Obsidian after build const copyPlugin = { @@ -27,6 +36,11 @@ const copyPlugin = { } try { + if (!OBSIDIAN_PLUGIN_DIR) { + console.log('ℹ️ OBSIDIAN_PLUGIN_DIR is not set, skipping copy to Obsidian vault'); + return; + } + // Ensure directory exists if (!fs.existsSync(OBSIDIAN_PLUGIN_DIR)) { fs.mkdirSync(OBSIDIAN_PLUGIN_DIR, { recursive: true }); diff --git a/packages/obsidian-plugin/src/main.ts b/packages/obsidian-plugin/src/main.ts index dc154df..67014cc 100644 --- a/packages/obsidian-plugin/src/main.ts +++ b/packages/obsidian-plugin/src/main.ts @@ -1,4 +1,4 @@ -import { Plugin } from 'obsidian'; +import { Plugin, parseYaml } from 'obsidian'; import { parse, validate, createLayout, renderSVG } from '@bytegrid/core'; export default class ByteGridPlugin extends Plugin { @@ -6,7 +6,8 @@ export default class ByteGridPlugin extends Plugin { this.registerMarkdownCodeBlockProcessor('bytegrid', (source, el, ctx) => { try { // Parse YAML input - const config = parse(source); + const parsed = parseYaml(source); + const config = parse(parsed); // Validate configuration validate(config); @@ -14,8 +15,33 @@ export default class ByteGridPlugin extends Plugin { // Create layout blocks const blocks = createLayout(config); - // Render to SVG - const svg = renderSVG(config, blocks); + // Render to SVG integrating with Obsidian's CSS variables + const svg = renderSVG(config, blocks, { + theme: { + background: 'transparent', + textNormal: 'var(--text-normal)', + textMuted: 'var(--text-muted)', + textFaint: 'var(--text-faint)', + border: 'var(--background-modifier-border)', + gridLine: 'var(--background-modifier-border)', + gridLineSubtle: 'var(--background-modifier-border-hover)', + // For block cell text, using var(--text-normal) might lack contrast against colored blocks. + // We use a high-contrast fallback variable or default to white/black via CSS if available. + // Obsidian's callouts use --callout-title-color which might be good, but for now we rely on standard text-muted. + // Let's omit cellText overrides to fallback to textNormal/textMuted, + palette: { + blue: 'var(--color-blue)', + cyan: 'var(--color-cyan)', + yellow: 'var(--color-yellow)', + green: 'var(--color-green)', + orange: 'var(--color-orange)', + purple: 'var(--color-purple)', + mint: 'var(--color-green)', // No mint in standard Obsidian, fallback to green + pink: 'var(--color-pink)', + gray: 'var(--color-base-40)', + } + } + }); // Display SVG safely using DOMParser const parser = new DOMParser();