mirror of
https://github.com/waaraawa/ByteGrid.git
synced 2026-07-22 06:41:47 +00:00
integrate obsidian API and native CSS variables
- Update license to the current year - Remove js-yaml dependency from core, switching to Obsidian's parseYaml API in the plugin - Add CustomTheme support to dynamically map Obsidian's CSS color variables to the SVG renderer for better theme compatibility - Extract hardcoded local vault path from esbuild config to a gitignored .env file
This commit is contained in:
parent
76c597622c
commit
880ca6bf93
9 changed files with 128 additions and 46 deletions
2
LICENSE
2
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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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<RenderOptions> = {
|
|||
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" style="font-family: monospace; background: white;">`;
|
||||
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" style="font-family: monospace; background: ${theme.background};">`;
|
||||
|
||||
// Title
|
||||
svg += `<text x="${totalWidth / 2}" y="30" text-anchor="middle" font-size="16" font-weight="bold">${escapeXml(config.name)}</text>`;
|
||||
svg += `<text x="${totalWidth / 2}" y="30" text-anchor="middle" font-size="16" font-weight="bold" fill="${theme.textNormal}">${escapeXml(config.name)}</text>`;
|
||||
|
||||
// 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 += `<text x="${x}" y="${y}" text-anchor="middle" font-size="9" fill="#666">${headerLabel}</text>`;
|
||||
svg += `<text x="${x}" y="${y}" text-anchor="middle" font-size="9" fill="${theme.textMuted}">${headerLabel}</text>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 += `<text x="${gridStartX - 5}" y="${textY}" text-anchor="end" font-size="9" fill="#666">${hexOffset}</text>`;
|
||||
svg += `<text x="${gridStartX - 5}" y="${textY}" text-anchor="end" font-size="9" fill="${theme.textMuted}">${hexOffset}</text>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 += `<rect x="${x}" y="${y}" width="${width}" height="${cellHeight}" fill="${color}" stroke="#333" stroke-width="1"/>`;
|
||||
svg += `<rect x="${x}" y="${y}" width="${width}" height="${cellHeight}" fill="${color}" stroke="${theme.border}" stroke-width="1"/>`;
|
||||
|
||||
// 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 += `<text x="${cellX}" y="${cellY}" text-anchor="middle" font-size="${opts.fontSize}" fill="#333">${hexNum}</text>`;
|
||||
const textFill = theme.cellText || theme.textNormal;
|
||||
svg += `<text x="${cellX}" y="${cellY}" text-anchor="middle" font-size="${opts.fontSize}" fill="${textFill}">${hexNum}</text>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 += `<line x1="${lineX}" y1="${y}" x2="${lineX}" y2="${y + cellHeight}" stroke="#ccc" stroke-width="0.5"/>`;
|
||||
svg += `<line x1="${lineX}" y1="${y}" x2="${lineX}" y2="${y + cellHeight}" stroke="${theme.gridLine}" stroke-width="0.5"/>`;
|
||||
}
|
||||
|
||||
// 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 += `<text x="${bitX}" y="${bitY}" text-anchor="middle" font-size="7" fill="#666">${7 - bit}</text>`;
|
||||
const bitTextFill = theme.cellTextMuted || theme.textMuted;
|
||||
svg += `<text x="${bitX}" y="${bitY}" text-anchor="middle" font-size="7" fill="${bitTextFill}">${7 - bit}</text>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>`;
|
||||
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="${theme.textFaint}" stroke-width="1" stroke-dasharray="3,3"/>`;
|
||||
} else {
|
||||
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="#ddd" stroke-width="0.5" stroke-dasharray="2,2"/>`;
|
||||
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="${theme.gridLineSubtle}" stroke-width="0.5" stroke-dasharray="2,2"/>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -451,7 +477,7 @@ export function renderSVG(
|
|||
legendTitleY = legendStartY - 20;
|
||||
}
|
||||
|
||||
svg += `<text x="${legendX}" y="${legendTitleY}" font-size="12" font-weight="bold" fill="#333">Fields</text>`;
|
||||
svg += `<text x="${legendX}" y="${legendTitleY}" font-size="12" font-weight="bold" fill="${theme.textNormal}">Fields</text>`;
|
||||
|
||||
// Group blocks by field name to avoid duplicates
|
||||
const uniqueFields = new Map<string, LayoutBlock>();
|
||||
|
|
@ -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 += `<rect x="${x}" y="${y}" width="20" height="20" fill="${color}" stroke="#333" stroke-width="1"/>`;
|
||||
svg += `<rect x="${x}" y="${y}" width="20" height="20" fill="${color}" stroke="${theme.border}" stroke-width="1"/>`;
|
||||
|
||||
// Field name
|
||||
svg += `<text x="${x + 30}" y="${y + 12}" font-size="11" font-weight="bold" fill="#333">${escapeXml(fieldName)}</text>`;
|
||||
svg += `<text x="${x + 30}" y="${y + 12}" font-size="11" font-weight="bold" fill="${theme.textNormal}">${escapeXml(fieldName)}</text>`;
|
||||
|
||||
// Type
|
||||
svg += `<text x="${x + 30}" y="${y + 24}" font-size="9" fill="#666">${escapeXml(block.fieldType)}</text>`;
|
||||
svg += `<text x="${x + 30}" y="${y + 24}" font-size="9" fill="${theme.textMuted}">${escapeXml(block.fieldType)}</text>`;
|
||||
|
||||
// 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 += `<text x="${x + 30}" y="${y + 36}" font-size="9" fill="#999">offset: ${offsetStartStr}-${offsetEndStr} (${size} ${unit})</text>`;
|
||||
svg += `<text x="${x + 30}" y="${y + 36}" font-size="9" fill="${theme.textFaint}">offset: ${offsetStartStr}-${offsetEndStr} (${size} ${unit})</text>`;
|
||||
|
||||
// Bitfields (if any)
|
||||
if (block.bitfields && block.bitfields.length > 0) {
|
||||
svg += `<text x="${x + 30}" y="${y + 48}" font-size="9" font-weight="bold" fill="#666">Bits:</text>`;
|
||||
svg += `<text x="${x + 30}" y="${y + 48}" font-size="9" font-weight="bold" fill="${theme.textMuted}">Bits:</text>`;
|
||||
|
||||
block.bitfields.forEach((bf, bfIndex) => {
|
||||
const bfY = y + 60 + bfIndex * 12;
|
||||
svg += `<text x="${x + 35}" y="${bfY}" font-size="8" fill="#888">`;
|
||||
svg += `<text x="${x + 35}" y="${bfY}" font-size="8" fill="${theme.textFaint}">`;
|
||||
svg += `bit ${escapeXml(bf.bits)}: ${escapeXml(bf.name)}`;
|
||||
svg += `</text>`;
|
||||
});
|
||||
|
|
@ -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 += `<text x="${totalWidth / 2}" y="${footerY}" text-anchor="middle" font-size="11" fill="#999">Total size: ${config.size} ${sizeUnit} | Layout: ${layout} ${layoutUnitText}</text>`;
|
||||
svg += `<text x="${totalWidth / 2}" y="${footerY}" text-anchor="middle" font-size="11" fill="${theme.textFaint}">Total size: ${config.size} ${sizeUnit} | Layout: ${layout} ${layoutUnitText}</text>`;
|
||||
}
|
||||
|
||||
svg += `</svg>`;
|
||||
|
|
|
|||
|
|
@ -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<Record<ColorName, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
2
packages/obsidian-plugin/.env.example
Normal file
2
packages/obsidian-plugin/.env.example
Normal file
|
|
@ -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
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue